diff --git a/.build/build.yaml b/.build/build.yaml new file mode 100644 index 0000000..37c3f01 --- /dev/null +++ b/.build/build.yaml @@ -0,0 +1,397 @@ +apiVersion: builds.katanomi.dev/v1alpha1 +kind: Build +spec: + workspaces: + - description: | + This workspace is shared among all the pipeline tasks to read/write common resources + name: source + - name: config + runTemplate: + spec: + workspaces: + - name: config + configmap: + name: buildkitd-config + taskRunSpecs: + - pipelineTaskName: operator-arm64 + taskPodTemplate: + nodeSelector: + kubernetes.io/arch: arm64 + Tolerations: + - key: build-arm + operator: Exists + effect: NoSchedule + - pipelineTaskName: expose-arm64 + taskPodTemplate: + nodeSelector: + kubernetes.io/arch: arm64 + Tolerations: + - key: build-arm + operator: Exists + effect: NoSchedule + - pipelineTaskName: operator-arm64 + stepOverrides: + - name: build + resources: + requests: + cpu: 1000m + memory: 2000Mi + limits: + cpu: 2000m + memory: 4000Mi + - name: push + resources: + requests: + cpu: 1000m + memory: 2000Mi + limits: + cpu: 2000m + memory: 4000Mi + - pipelineTaskName: bundle-arm64 + taskPodTemplate: + nodeSelector: + kubernetes.io/arch: arm64 + Tolerations: + - key: build-arm + operator: Exists + effect: NoSchedule + - pipelineTaskName: bundle-operator + stepOverrides: + - name: update-version-data + resources: + requests: + cpu: 666m + memory: 1333Mi + limits: + cpu: 2000m + memory: 4000Mi + - name: generate-related-images + resources: + requests: + cpu: 666m + memory: 1333Mi + limits: + cpu: 2000m + memory: 4000Mi + - name: operator-bundle + resources: + requests: + cpu: 668m + memory: 1334Mi + limits: + cpu: 2000m + memory: 4000Mi + - pipelineTaskName: operator-amd64 + stepOverrides: + - name: build + resources: + requests: + cpu: 1000m + memory: 2000Mi + limits: + cpu: 2000m + memory: 4000Mi + - name: push + resources: + requests: + cpu: 1000m + memory: 2000Mi + limits: + cpu: 2000m + memory: 4000Mi + - pipelineTaskName: go-test + stepOverrides: + - name: prepare + resources: + requests: + cpu: 500m + memory: 1000Mi + limits: + cpu: 1500m + memory: 3000Mi + - name: test + resources: + requests: + cpu: 500m + memory: 1000Mi + limits: + cpu: 1500m + memory: 3000Mi + - name: analysis + resources: + requests: + cpu: 500m + memory: 1000Mi + limits: + cpu: 1500m + memory: 3000Mi + - pipelineTaskName: operator-merge + stepOverrides: + - name: merge + resources: + requests: + cpu: 1000m + memory: 2000Mi + limits: + cpu: 1000m + memory: 2000Mi + tasks: + - name: go-lint + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: golangci-lint + workspaces: + - name: source + workspace: source + - name: cache + workspace: source + params: + - name: tool-image + value: registry.alauda.cn:60080/devops/builder-go-121:latest + - name: command + value: > + export GOPROXY=https://build-nexus.alauda.cn/repository/golang/,https://goproxy.cn,direct + + export GOMAXPROCS=4 + + golangci-lint --concurrency=4 run --verbose + - name: quality-gate + value: "true" + - name: quality-gate-rules + value: + - issues-count=0 + - name: go-test + runAfter: + - go-lint + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: go-unit-test + workspaces: + - name: source + workspace: source + params: + - name: command + value: |- + export GOPROXY="https://build-nexus.alauda.cn/repository/golang/,https://goproxy.cn,direct" + export GONOSUMDB="gitlab-ce.alauda.cn/*,gomod.alauda.cn/*,bitbucket.org/mathildetech/*" + make test + - name: coverage-report-path + value: ./coverage.txt + - name: quality-gate + value: "false" + - name: tool-image + value: docker-mirrors.alauda.cn/library/golang:1.21.8 + - name: code-scan + runAfter: + - go-test + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: sonarqube-analysis + workspaces: + - name: source + workspace: source + params: + - name: server + value: https://build-sonar.alauda.cn + - name: quality-gate + value: "false" + - name: operator-amd64 + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: build-image-buildkit + workspaces: + - name: source + workspace: source + - name: config + workspace: config + params: + - name: container-images + value: + - build-harbor.alauda.cn/middleware/redis-operator + - name: labels + value: + - branch=$(build.git.branch.name) + - commit=$(build.git.lastCommit.id) + - name: operator-arm64 + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: build-image-buildkit + workspaces: + - name: source + workspace: source + - name: config + workspace: config + params: + - name: container-images + value: + - build-harbor.alauda.cn/middleware/redis-operator + - name: labels + value: + - branch=$(build.git.branch.name) + - commit=$(build.git.lastCommit.id) + - name: operator-merge + runAfter: + - operator-amd64 + - operator-arm64 + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: merge-image-buildkit + workspaces: + - name: source + workspace: source + params: + - name: container-images + value: + - build-harbor.alauda.cn/middleware/redis-operator:$(build.git.version.docker) + - name: source-image-digests + value: + - $(tasks.operator-amd64.results.ociContainerImageBuild-url) + - $(tasks.operator-arm64.results.ociContainerImageBuild-url) + - name: update-values-yaml + runAfter: + - operator-merge + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: alauda-update-chart-dependencies + workspaces: + - name: source + workspace: source + when: [] + params: + - name: chart-file-path + value: ./ + - name: branch + value: $(build.git.branch) + - name: verbose + value: "true" + - name: bundle-operator + runAfter: + - update-values-yaml + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: alauda-operator-bundle + workspaces: + - name: source + workspace: source + params: + - name: set-skip-range + value: ">=0.0.0 <$(build.git.version.clean)" + - name: build-command + value: curl https://build-nexus.alauda.cn/repository/alauda/middleware/operator-sdk_linux/operator-sdk_1_33_linux_$(arch) -o $(go env GOPATH)/bin/operator-sdk && chmod +x $(go env GOPATH)/bin/operator-sdk && make bundle + - name: controller-version + value: $(build.git.version.docker) + - name: bundle-version + value: $(build.git.version.clean) + - name: csv-file-path + value: config/manifests/bases/redis-operator.clusterserviceversion.yaml + - name: bundle-csv-file-path + value: bundle/manifests/redis-operator.clusterserviceversion.yaml + - name: controller-values-filepath + value: values.yaml + - name: controller-values-jsonpath + value: global.images.redis-operator.tag + - name: bundle-amd64 + runAfter: + - bundle-operator + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: build-image-buildkit + workspaces: + - name: source + workspace: source + params: + - name: container-images + value: + - build-harbor.alauda.cn/middleware/redis-operator-bundle + - name: dockerfile + value: bundle.Dockerfile + - name: labels + value: + - branch=$(build.git.branch.name) + - app_version=$(build.git.branch.name) + - commit=$(build.git.lastCommit.id) + - name: bundle-arm64 + runAfter: + - bundle-operator + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: build-image-buildkit + workspaces: + - name: source + workspace: source + params: + - name: container-images + value: + - build-harbor.alauda.cn/middleware/redis-operator-bundle + - name: dockerfile + value: bundle.Dockerfile + - name: labels + value: + - branch=$(build.git.branch.name) + - app_version=$(build.git.branch.name) + - commit=$(build.git.lastCommit.id) + - name: bundle-merge + runAfter: + - bundle-amd64 + - bundle-arm64 + - code-scan + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: merge-image-buildkit + workspaces: + - name: source + workspace: source + params: + - name: container-images + value: + - build-harbor.alauda.cn/middleware/redis-operator-bundle:$(build.git.version.docker) + - name: source-image-digests + value: + - $(tasks.bundle-amd64.results.ociContainerImageBuild-url) + - $(tasks.bundle-arm64.results.ociContainerImageBuild-url) + finally: + - name: release-tag + when: + - input: "$(build.git.versionPhase)" + operator: in + values: + - custom + - ga + - input: $(tasks.status) + operator: in + values: + - Succeeded + - Completed + timeout: 30m + retries: 0 + taskRef: + kind: ClusterTask + name: alauda-release-tag + workspaces: + - name: source + workspace: source + params: + - name: version + value: $(build.git.version.docker) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index be8ccc3..98faaf5 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -1,37 +1,18 @@ name: dev -on: - push: - branch: [ "main" ] - pull_request: +on: [pull_request] jobs: - check: - name: Golang Check + test: + name: Test with Coverage runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - cache: false - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: v1.54 - args: --timeout=15m --tests=false --exclude-use-default - unit-test: - name: Unit Test - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v2 with: - go-version-file: go.mod - cache: false + go-version: '1.22' + - name: Check out code + uses: actions/checkout@v2 - name: Install dependencies run: | go mod download diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9e01553..c63c8e0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,7 @@ jobs: name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.20 + go-version: 1.22 - name: Docker Login uses: docker/login-action@v3 @@ -47,4 +47,4 @@ jobs: version: latest args: release --rm-dist env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.gitignore b/.gitignore index 4de8294..78fb8ca 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ bin/ coverage.txt bundle/* bundle.Dockerfile +golangci-lint-report.xml diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..749378c --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,80 @@ +output: + formats: colored-line-number,checkstyle:./golangci-lint-report.xml +run: + issues-exit-code: 0 + timeout: 10m + build-tags: [] + go: "1.21" +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + - gosec +linters-settings: + nestif: + min-complexity: 12 + prealloc: + range-loops: true + for-loops: true + govet: + enable-all: true + disable: + - fieldalignment + - shadow + revive: + rules: + - name: var-declaration + disabled: true + stylecheck: + checks: ["ST1000", "ST1019", "ST1020", "ST1021", "ST1022"] + http-status-code-whitelist: ["200", "400", "404", "500"] + gosec: + includes: + - G101 + - G102 + - G103 + - G104 + - G106 + - G107 + - G108 + - G109 + - G110 + - G111 + - G201 + - G202 + - G203 + - G204 + - G301 + - G302 + - G303 + - G304 + - G305 + - G306 + - G307 + - G401 + - G402 + - G403 + - G404 + - G501 + - G502 + - G503 + - G504 + - G505 + - G601 +issues: + exclude-rules: + - path: test # Excludes /test, *_test.go etc. + linters: + - gosec + - unparam + exclude-dirs: + - config + - test + exclude-files: + - ".*/go/pkg/mod/.*" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2288deb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## GitHub issues + +Redis Operator uses GitHub issues for feature development and bug tracking. +The issues have specific information as to what the feature should do and what problem or +use case is trying to resolve. Bug reports have a description of the actual behaviour and +the expected behaviour, along with repro steps when possible. It is important to provide +repro when possible, as it speeds up the triage and potential fix. + +For support questions, we strongly encourage you to provide a way to +reproduce the behavior you're observing, or at least sharing as much +relevant information as possible on the Issue. This would include YAML manifests, Kubernetes version, +Redis Operator debug logs and any other relevant information that might help +to diagnose the problem. + +## Makefile + +This project contains a Makefile to perform common development operation. If you want to build, test or deploy a local copy of the repository, keep reading. + +### Required environment variables + +The following environment variables are required by many of the `make` targets to access a custom-built image: + +- DOCKER_REGISTRY_SERVER: URL of docker registry containing the Operator image (e.g. `registry.my-company.com`) +- OPERATOR_IMAGE: path to the Operator image within the registry specified in DOCKER_REGISTRY_SERVER (e.g. `alauda/redis-operator`). Note: OPERATOR_IMAGE should **not** include a leading slash (`/`) + +When running `make deploy`, additionally: + +- DOCKER_REGISTRY_USERNAME: Username for accessing the docker registry +- DOCKER_REGISTRY_PASSWORD: Password for accessing the docker registry +- DOCKER_REGISTRY_SECRET: Name of Kubernetes secret in which to store the Docker registry username and password + +#### Make targets + +- **controller-gen** Install controller-gen if not in local bin path +- **kustomize** Install kustomize if not in local bin path +- **envtest** Install setup-envtest tools if not in local bin path +- **operator-sdk** Install operator-sdk CLI if not in local bin path +- **manifests** Generate manifests e.g. CRD, RBAC etc. +- **generate** Generate code +- **fmt** Run go fmt against code +- **vet** Run go vet against code +- **test** Run unit tests +- **integration-tests** Run integration tests (TODO) +- **build** Build operator binary +- **run** Run operator binary locally against the configured Kubernetes cluster in ~/.kube/config +- **docker-build** Build the docker image +- **docker-push** Push the docker image +- **docker-buildx** Build the docker image using buildx +- **install** Install CRDs into a cluster +- **uninstall** Uninstall CRDs from a cluster +- **deploy** Deploy operator in the configured Kubernetes cluster in ~/.kube/config +- **undeploy** Undeploy operator from the configured Kubernetes cluster in ~/.kube/config +- **bundle** Generate bundle manifests and metadata, then validate generated manifests +- **bundle-build** Build the bundle image +- **bundle-push** Push the bundle image + +### Testing + +Before submitting a pull request, ensure necessary unit tests added and all local tests pass: +- `make test` + +Also, run the system tests with your local changes against a Kubernetes cluster: +- `make deploy` + +## Pull Requests + +Redis Operator project uses pull requests to discuss, collaborate on and accept code contributions. +Pull requests are the primary place of discussing code changes. + +Here's the recommended workflow: + + * [Fork the repository][github-fork] or repositories you plan on contributing to. If multiple + repositories are involved in addressing the same issue, please use the same branch name + in each repository + * Create a branch with a descriptive name + * Make your changes, run tests (usually with `make test`), commit with a + [descriptive message][git-commit-msgs], push to your fork + * Submit pull requests with an explanation what has been changed and **why** + * We will get to your pull request within one week. Usually within the next day or two you'll get a response. + +### Code Conventions + +This project follows the [Kubernetes Code Conventions for Go](https://github.com/kubernetes/community/blob/master/contributors/guide/coding-conventions.md#code-conventions), which in turn mostly refer to [Effective Go](https://golang.org/doc/effective_go.html) and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). Please ensure your pull requests follow these guidelines. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/Dockerfile b/Dockerfile index 5f5450e..529e7c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,31 @@ -FROM golang:1.20 as builder +FROM golang:1.22-alpine as builder + WORKDIR /workspace -COPY . . +# Dependencies are cached unless we change go.mod or go.sum +COPY go.mod go.mod +COPY go.sum go.sum +RUN GOPROXY=https://goproxy.io,direct go mod download -ENV GOPROXY=https://goproxy.io,direct -# Build -RUN CGO_ENABLED=1 go build -buildmode=pie -ldflags '-extldflags "-Wl,-z,relro,-z,now" -linkmode=external -w -s' -a -o manager cmd/main.go && go build -buildmode=pie -ldflags '-extldflags "-Wl,-z,relro,-z,now" -linkmode=external -w -s' -a -o redis-tools cmd/redis-tools/main.go +# Copy the go source +COPY api/ api/ +COPY cmd/ cmd/ +COPY internal/ internal/ +COPY pkg/ pkg/ +COPY tools/ tools/ -FROM redis:7.2-alpine -COPY --from=builder /workspace/manager . -COPY --from=builder /workspace/redis-tools /opt/ -COPY --from=builder /workspace/cmd/redis-tools/scripts/ /opt/ +RUN CGO_ENABLED=0 go build -a -tags timetzdata -buildmode=pie -ldflags '-extldflags "-Wl,-z,relro,-z,now"' -a -o manager cmd/main.go +RUN CGO_ENABLED=0 go build -a -tags timetzdata -buildmode=pie -ldflags '-extldflags "-Wl,-z,relro,-z,now"' -a -o redis-tools cmd/redis-tools/main.go + + +FROM redis:7.2-alpine RUN apk add --no-cache gcompat -RUN ARCH= &&dpkgArch="$(arch)" \ - && case "${dpkgArch}" in \ - x86_64) ARCH='amd64';; \ - aarch64) ARCH='arm64';; \ - *) echo "unsupported architecture"; exit 1 ;; \ - esac \ - && wget https://storage.googleapis.com/kubernetes-release/release/v1.28.3/bin/linux/${ARCH}/kubectl -O /bin/kubectl && chmod +x /bin/kubectl +COPY --from=builder /workspace/manager . +COPY --from=builder /workspace/redis-tools /opt/ +COPY --from=builder /workspace/tools/ /opt/ ENV PATH="/opt:$PATH" diff --git a/Makefile b/Makefile index f530e8a..35f2614 100644 --- a/Makefile +++ b/Makefile @@ -3,22 +3,12 @@ REGISTRY ?= ghcr.io # GROUP defines the docker image group GROUP ?= alauda -REDIS_IMAGE ?= redis -REDIS_IMAGE_VERSION_6 ?= 6.0-alpine -REDIS_IMAGE_VERSION_6_2 ?= 6.2-alpine -REDIS_IMAGE_VERSION_7 ?= 7.0-alpine -REDIS_IMAGE_VERSION_7_2 ?= 7.2-alpine -REDIS_IMAGE_VERSION_DEFAULT ?= $(REDIS_IMAGE_VERSION_6) - -EXPORTER_IMAGE ?= oliver006/redis_exporter -EXPORTER_VERSION ?= v1.55.0 - # VERSION defines the project version for the bundle. # Update this value when you upgrade the version of your project. # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 1.0.0 +VERSION ?= 3.18.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") @@ -149,7 +139,7 @@ docker-push: ## Push docker image with the manager. # - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ # - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) # To properly provided solutions that supports more than one platform you should use this option. -PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +PLATFORMS ?= linux/arm64,linux/amd64 .PHONY: docker-buildx docker-buildx: test ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile @@ -198,7 +188,7 @@ ENVTEST ?= $(LOCALBIN)/setup-envtest ## Tool Versions KUSTOMIZE_VERSION ?= v5.0.1 -CONTROLLER_TOOLS_VERSION ?= v0.12.0 +CONTROLLER_TOOLS_VERSION ?= v0.16.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. @@ -241,12 +231,6 @@ endif bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. @cd config/manager && $(KUSTOMIZE) edit set image redis-operator=$(IMG) $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS) - @sed -i 's#__DEFAULT_REDIS_IMAGE__#$(REDIS_IMAGE):$(REDIS_IMAGE_VERSION_DEFAULT)#g' ./bundle/manifests/redis-operator.clusterserviceversion.yaml - @sed -i 's#__REDIS_VERSION_6_IMAGE__#$(REDIS_IMAGE):$(REDIS_IMAGE_VERSION_6)#g' ./bundle/manifests/redis-operator.clusterserviceversion.yaml - @sed -i 's#__REDIS_VERSION_6_2_IMAGE__#$(REDIS_IMAGE):$(REDIS_IMAGE_VERSION_6_2)#g' ./bundle/manifests/redis-operator.clusterserviceversion.yaml - @sed -i 's#__REDIS_VERSION_7_IMAGE__#$(REDIS_IMAGE):$(REDIS_IMAGE_VERSION_7)#g' ./bundle/manifests/redis-operator.clusterserviceversion.yaml - @sed -i 's#__REDIS_VERSION_7_2_IMAGE__#$(REDIS_IMAGE):$(REDIS_IMAGE_VERSION_7_2)#g' ./bundle/manifests/redis-operator.clusterserviceversion.yaml - @sed -i 's#__DEFAULT_EXPORTER_IMAGE__#$(EXPORTER_IMAGE):$(EXPORTER_VERSION)#g' ./bundle/manifests/redis-operator.clusterserviceversion.yaml @sed -i 's#__CURRENT_VERSION__#$(VERSION)#g' ./bundle/manifests/redis-operator.clusterserviceversion.yaml $(OPERATOR_SDK) bundle validate ./bundle diff --git a/PROJECT b/PROJECT index e4ab946..28a8ff9 100644 --- a/PROJECT +++ b/PROJECT @@ -7,7 +7,6 @@ layout: multigroup: true plugins: manifests.sdk.operatorframework.io/v2: {} - scorecard.sdk.operatorframework.io/v2: {} projectName: redis-operator repo: github.com/alauda/redis-operator resources: @@ -15,49 +14,50 @@ resources: crdVersion: v1 namespaced: true controller: true - group: databases.spotahome.com + domain: spotahome.com + group: databases kind: RedisFailover - path: github.com/alauda/redis-operator/api/databases.spotahome.com/v1 - plural: rf + path: github.com/alauda/redis-operator/api/databases/v1 version: v1 - api: crdVersion: v1 namespaced: true controller: true - group: redis.kun - kind: DistributedRedisCluster - path: github.com/alauda/redis-operator/api/redis.kun/v1alpha1 - plural: drc - version: v1alpha1 + domain: spotahome.com + group: databases + kind: RedisSentinel + path: github.com/alauda/redis-operator/api/databases/v1 + version: v1 - api: crdVersion: v1 namespaced: true controller: true - domain: middleware.alauda.io - group: redis - kind: RedisBackup - path: github.com/alauda/redis-operator/api/redis/v1 - plural: rb - version: v1 + domain: redis.kun + group: cluster + kind: DistributedRedisCluster + path: github.com/alauda/redis-operator/api/cluster/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 namespaced: true controller: true - domain: middleware.alauda.io - group: redis - kind: RedisClusterBackup - path: github.com/alauda/redis-operator/api/redis/v1 - plural: rcb + domain: alauda.io + group: middleware + kind: Redis + path: github.com/alauda/redis-operator/api/middleware/v1 version: v1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true controller: true - domain: middleware.alauda.io - group: redis + domain: alauda.io + group: redis.middleware kind: RedisUser - path: github.com/alauda/redis-operator/api/redis/v1 - plural: ru + path: github.com/alauda/redis-operator/api/middleware/redis/v1 version: v1 webhooks: defaulting: true diff --git a/README.md b/README.md index d64a1ba..3b851da 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # RedisOperator [![Coverage Status](https://coveralls.io/repos/github/alauda/redis-operator/badge.svg?branch=main)](https://coveralls.io/github/alauda/redis-operator?branch=main) -**RedisOperator** is a production-ready kubernetes operator to deploy and manage high available [Redis Sentinel](https://redis.io/docs/management/sentinel/) and [Redis Cluster](https://redis.io/docs/reference/cluster-spec/) instances. This repository contains multi [Custom Resource Definition (CRD)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) designed for the lifecycle of Redis sentinel or cluster instance. +**RedisOperator** is a production-ready kubernetes operator to deploy and manage high available [Redis Sentinel](https://redis.io/docs/management/sentinel/) and [Redis Cluster](https://redis.io/docs/reference/cluster-spec/) instances. This repository contains multi [Custom Resource Definition (CRD)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) designed for the lifecycle of Redis standalone, sentinel or cluster instance. ## Features -* Redis sentinel/cluster supported -* ACL supported -* Redis 6.0, 6.2, 7.0, 7.2 supported (only versions 6.0 and 7.2 have undergone thorough testing. 5.0 also supported, but no acl supported) -* Nodeport access supported, assigne ports also supported -* IPv4/IpV6 supported -* Online scale up/down -* Online data backup/restore -* Graceful version upgrade -* Nodeselector, toleration and affinity supported -* High available in production environment +* Standalone/Sentinel/Cluster redis arch supported. +* Redis ACL supported. +* Redis 6.0, 6.2, 7.0, 7.2, 7.4 supported (only versions 6.0 and 7.2 have undergone thorough testing. 5.0 also supported, but no acl supported). +* Nodeport/LB access supported; nodeport assignement also supported. +* IPv4/IPv6 supported. +* Online scale up/down. +* Graceful version upgrade. +* Nodeselector, toleration and affinity supported. +* High available in production environment. ## Quickstart @@ -29,17 +28,16 @@ RedisOperator is covered by following topics: * **TODO** Deploying the operator * **TODO** Deploying a Redis sentinel/cluster instance * **TODO** Monitoring the instance -* **TODO** Backup instance data In addition, few [samples](./config/samples) can be find in this repo. ## Contributing -This project follows the typical GitHub pull request model. Before starting any work, please either comment on an [existing issue](https://github.com/alauda/redis-operator/issues), or file a new one. +This project follows the typical GitHub pull request model. Before starting any work, please either comment on an [existing issue](https://github.com/alauda/redis-operator/issues), or file a new one. For more details, please refer to the [CONTRIBUTING.md](./CONTRIBUTING.md) file. ## Releasing -To release a new version of the RedisOperator, create a versioned tag (e.g. `v1.2.3`) of the repo, and the release pipeline will generate a new draft release, along side release artefacts. +To release a new version of the RedisOperator, create a versioned tag (e.g. `v3.18.0`) of the repo, and the release pipeline will generate a new draft release, along side release artefacts. ## License diff --git a/VERSION_GUIDELINES.md b/VERSION_GUIDELINES.md new file mode 100644 index 0000000..7ce4b55 --- /dev/null +++ b/VERSION_GUIDELINES.md @@ -0,0 +1,32 @@ +### Redis Operator Versioning Scheme +--- +Redis Operator follows non-strict semver. This document explains the non-strict semver versioning scheme used by Redis Operator. + +_**Operator** refers to *Redis Operator* in the document henceforth._ + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in +this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +:warning: **Please note that these are only guidelines which MAY NOT be always followed. Users MUST read the release notes to understand the changes and the potential impact of these changes.** + +1. A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative integers, and MUST NOT contain leading zeroes. +X is the major version, Y is the minor version, and Z is the patch version. Each element MUST increase numerically. For instance: 1.9.0 -> 1.10.0 -> 1.11.0. + +2. Major version zero (0.y.z) is for initial development. This is a Beta version and anything MAY change at anytime. The Operator SHOULD NOT be considered stable. + +3. Version 1.0.0 defines the GA/stable version of Operator. The way in which the version number is incremented after this release is dependent +on how the Operator changes. + +4. Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes and/or CVE fixes are introduced. +A bug fix is defined as an internal change that fixes incorrect behavior. + +5. Minor version Y (x.Y.z | x > 0): +- It MUST be incremented if new, functionality is introduced to the Operator. The new functionality MAY contain breaking changes. +- It MUST be incremented if any OPERATOR functionality is marked as deprecated. +- It MUST be incremented if underlying Kubernetes server version and/or Redis/Valkey version are marked as supported and/or deprecated. +- It MAY contain breaking changes. Breaking changes MUST be documented in the release notes. +- It MAY include patch level changes. Patch version MUST be reset to 0 when minor version is incremented. + +6. Major version X (X.y.z | X > 0): +- It MAY be incremented if any backwards incompatible changes are introduced to the public API. +- It MAY also include minor and patch level changes. Patch and minor version MUST be reset to 0 when major version is incremented. diff --git a/VERSION_SUPPORTED.md b/VERSION_SUPPORTED.md new file mode 100644 index 0000000..e5ce7c5 --- /dev/null +++ b/VERSION_SUPPORTED.md @@ -0,0 +1,22 @@ +# Supported versions +--- + +## Supported Redis Versions + +| Redis Version | Supported | Tested | +|---------------|-----------|-----------| +| 4.0.x | No | | +| 5.0.x | No | | +| 6.0.x | Yes | Yes | +| 6.2.x | Yes | | +| 7.0.x | Yes | | +| 7.2.x | Yes | Yes | +| 7.4.x | Yes | | + +## Supported Valkey Versions + +TODO + +## Supported Kubernetes Versions + +TODO diff --git a/api/redis.kun/v1alpha1/distributedrediscluster_types.go b/api/cluster/v1alpha1/distributedrediscluster_types.go similarity index 52% rename from api/redis.kun/v1alpha1/distributedrediscluster_types.go rename to api/cluster/v1alpha1/distributedrediscluster_types.go index 5993b13..a579928 100644 --- a/api/redis.kun/v1alpha1/distributedrediscluster_types.go +++ b/api/cluster/v1alpha1/distributedrediscluster_types.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,24 +17,14 @@ limitations under the License. package v1alpha1 import ( - redisfailoverv1alpha1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/api/core" + + smv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// AffinityPolicy -type AffinityPolicy string - -const ( - // SoftAntiAffinity the master and slave will be scheduled on different node if possible - SoftAntiAffinity AffinityPolicy = "SoftAntiAffinity" - // AntiAffinityInSharding the master and slave must be scheduled on different node - AntiAffinityInSharding AffinityPolicy = "AntiAffinityInSharding" - // AntiAffinity all redis pods must be scheduled on different node - AntiAffinity AffinityPolicy = "AntiAffinity" -) - // StorageType type StorageType string @@ -43,11 +33,38 @@ const ( Ephemeral StorageType = "ephemeral" ) +// RedisServiceMonitorSpec +type RedisServiceMonitorSpec struct { + CustomMetricRelabelings bool `json:"customMetricRelabelings,omitempty"` + MetricRelabelConfigs []*smv1.RelabelConfig `json:"metricRelabelings,omitempty"` + Interval string `json:"interval,omitempty"` + ScrapeTimeout string `json:"scrapeTimeout,omitempty"` +} + +// PrometheusSpec +// +// this struct must be Deprecated, only port is used. +type PrometheusSpec struct { + // Port number for the exporter side car. + Port int32 `json:"port,omitempty"` + + // Namespace of Prometheus. Service monitors will be created in this namespace. + Namespace string `json:"namespace,omitempty"` + // Labels are key value pairs that is used to select Prometheus instance via ServiceMonitor labels. + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // Interval at which metrics should be scraped + Interval string `json:"interval,omitempty"` + //Annotations map[string]string `json:"annotations,omitempty"` +} + // Monitor type Monitor struct { - // Image monitor image - Image string `json:"image,omitempty"` + Image string `json:"image,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + Prometheus *PrometheusSpec `json:"prometheus,omitempty"` // Arguments to the entrypoint. // The docker image's CMD is used if this is not provided. // Variable references $(VAR_NAME) are expanded using the container's environment. If a variable @@ -82,79 +99,98 @@ type ClusterShardConfig struct { Slots string `json:"slots,omitempty"` } +// RedisStorage defines the structure used to store the Redis Data +type RedisStorage struct { + Size resource.Quantity `json:"size"` + Type StorageType `json:"type,omitempty"` + Class string `json:"class"` + DeleteClaim bool `json:"deleteClaim,omitempty"` +} + // DistributedRedisClusterSpec defines the desired state of DistributedRedisCluster type DistributedRedisClusterSpec struct { - // Image is the Redis image to run. + // Image is the Redis image Image string `json:"image,omitempty"` - // ImagePullPolicy is the pull policy for the Redis image. - // +kubebuilder:validation:Enum=Always;Never;IfNotPresent - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` - // ImagePullSecrets is the list of pull secrets for the Redis image. + // ImagePullPolicy is the Redis image pull policy + // TODO: reset the default value to IfNotPresent in 3.20 + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` - // Command is the Redis image command. - Command []string `json:"command,omitempty"` - // Env inject envs to redis pods. + Command []string `json:"command,omitempty"` + // Env is the environment variables + // TODO: remove in 3.20 Env []corev1.EnvVar `json:"env,omitempty"` - // MasterSize is the number of shards - MasterSize int32 `json:"masterSize,omitempty"` - // ClusterReplicas is the number of replicas for each shard - ClusterReplicas int32 `json:"clusterReplicas,omitempty"` - // ServiceName is the name of the statefulset + // MasterSize is the number of master nodes + // +kubebuilder:validation:Minimum=3 + MasterSize int32 `json:"masterSize"` + // ClusterReplicas is the number of replicas for each master node + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=5 + ClusterReplicas int32 `json:"clusterReplicas"` + // This field specifies the assignment of cluster shard slots. + // this config is only works for new create instance, update will not take effect after instance is startup + Shards []ClusterShardConfig `json:"shards,omitempty"` + + // ServiceName is the service name + // TODO: remove in 3.20, this should not changed or specified ServiceName string `json:"serviceName,omitempty"` // Use this map to setup redis service. Most of the settings is key-value format. // // For client-output-buffer-limit and rename, the values is split by group. Config map[string]string `json:"config,omitempty"` - // This field specifies the assignment of cluster shard slots. - // this config is only works for new create instance, update will not take effect after instance is startup - Shards []ClusterShardConfig `json:"shards,omitempty"` // AffinityPolicy // +kubebuilder:validation:Enum=SoftAntiAffinity;AntiAffinityInSharding;AntiAffinity - AffinityPolicy AffinityPolicy `json:"affinityPolicy,omitempty"` + AffinityPolicy core.AffinityPolicy `json:"affinityPolicy,omitempty"` + // Set RequiredAntiAffinity to force the master-slave node anti-affinity. + //+kubebuilder:deprecatedversion:warning="redis.kun/v1alpha2 DistributedRedisCluster is deprecated, use AffinityPolicy instead" + // TODO: remove in 3.20 + RequiredAntiAffinity bool `json:"requiredAntiAffinity,omitempty"` // Affinity - // +kubebuilder:deprecated:warning="This version is deprecated in favor of AffinityPolicy" + // TODO: remove in 3.20 Affinity *corev1.Affinity `json:"affinity,omitempty"` // NodeSelector NodeSelector map[string]string `json:"nodeSelector,omitempty"` // Tolerations Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - // SecurityContext for redis pods - SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` - // ContainerSecurityContext for redis container - ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"` - // Annotations annotations inject to redis pods - Annotations map[string]string `json:"annotations,omitempty"` - // Storage storage config for redis pods - Storage *RedisStorage `json:"storage,omitempty"` - // StorageType storage type for redis pods - Resources *corev1.ResourceRequirements `json:"resources,omitempty"` - // PasswordSecret password secret for redis pods - PasswordSecret *corev1.LocalObjectReference `json:"passwordSecret,omitempty"` + SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` + ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"` + PodAnnotations map[string]string `json:"annotations,omitempty"` + Storage *RedisStorage `json:"storage,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + PasswordSecret *corev1.LocalObjectReference `json:"passwordSecret,omitempty"` // Monitor + // TODO: added an global button to controller wether to enable monitor Monitor *Monitor `json:"monitor,omitempty"` // ServiceMonitor - ServiceMonitor redisfailoverv1alpha1.RedisServiceMonitorSpec `json:"serviceMonitor,omitempty"` - - // Backup and proxy - // - // TODO: refactor backup - // Backup and proxy should decoupling from DistributedRedisCluster. + //+kubebuilder:deprecatedversion + // not support setup service monitor for each instance + ServiceMonitor *RedisServiceMonitorSpec `json:"serviceMonitor,omitempty"` // Set backup schedule - Backup redisfailoverv1alpha1.RedisBackup `json:"backup,omitempty"` + Backup core.RedisBackup `json:"backup,omitempty"` // Restore restore redis data from backup - Restore redisfailoverv1alpha1.RedisRestore `json:"restore,omitempty"` - // EnableTLS enable TLS for redis + Restore core.RedisRestore `json:"restore,omitempty"` + + // EnableTLS EnableTLS bool `json:"enableTLS,omitempty"` + // Expose config for service access - Expose redisfailoverv1alpha1.RedisExpose `json:"expose,omitempty"` + // TODO: should rename Expose to Access + Expose core.InstanceAccess `json:"expose,omitempty"` + // IPFamilyPrefer the prefered IP family, enum: IPv4, IPv6 IPFamilyPrefer corev1.IPFamily `json:"ipFamilyPrefer,omitempty"` + + // EnableActiveRedis enable active-active model for Redis + EnableActiveRedis bool `json:"enableActiveRedis,omitempty"` + // ServiceID the service id for activeredis + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=15 + ServiceID *int32 `json:"serviceID,omitempty"` } // ClusterStatus Redis Cluster status @@ -215,28 +251,6 @@ const ( NodesPlacementInfoOptimal NodesPlacementInfo = "Optimal" ) -// RedisClusterNode represent a RedisCluster Node -type RedisClusterNode struct { - // ID id of redis-server - ID string `json:"id"` - // Role redis-server role - Role redis.RedisRole `json:"role"` - // IP current pod ip - IP string `json:"ip"` - // Port current pod port - Port string `json:"port"` - // Slots this master node holds - Slots []string `json:"slots,omitempty"` - // MasterRef referred to the master node - MasterRef string `json:"masterRef,omitempty"` - // PodName pod name - PodName string `json:"podName"` - // NodeName node name the pod hosted - NodeName string `json:"nodeName"` - // StatefulSet the statefulset current pod belongs - StatefulSet string `json:"statefulSet"` -} - // DistributedRedisClusterStatus defines the observed state of DistributedRedisCluster type DistributedRedisClusterStatus struct { // Status the status of the cluster @@ -252,19 +266,65 @@ type DistributedRedisClusterStatus struct { // NodesPlacement the nodes placement mode NodesPlacement NodesPlacementInfo `json:"nodesPlacementInfo,omitempty"` // Nodes the redis cluster nodes - Nodes []RedisClusterNode `json:"nodes,omitempty"` + Nodes []core.RedisNode `json:"nodes,omitempty"` // ClusterStatus the cluster status ClusterStatus ClusterServiceStatus `json:"clusterStatus,omitempty"` - // Shards the shards status + // Shards the cluster shards Shards []*ClusterShards `json:"shards,omitempty"` + + // DetailedStatusRef detailed status resource ref + DetailedStatusRef *corev1.ObjectReference `json:"detailedStatusRef,omitempty"` } -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:printcolumn:name="Shards",type="integer",JSONPath=".status.numberOfMaster",description="Current Shards" -//+kubebuilder:printcolumn:name="Service Status",type="string",JSONPath=".status.clusterStatus",description="Service status" -//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.status",description="Instance status" -//+kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.reason",description="Status message" +// DistributedRedisClusterDetailedStatus defines the detailed status of DistributedRedisCluster +type DistributedRedisClusterDetailedStatus struct { + // Status indicates current status of the cluster + Status ClusterStatus `json:"status"` + // Reason explains the status + Reason string `json:"reason,omitempty"` + // NumberOfMaster the number of master nodes + NumberOfMaster int32 `json:"numberOfMaster,omitempty"` + // MinReplicationFactor the min replication factor + MinReplicationFactor int32 `json:"minReplicationFactor,omitempty"` + // MaxReplicationFactor the max replication factor + MaxReplicationFactor int32 `json:"maxReplicationFactor,omitempty"` + // NodesPlacement the nodes placement mode + NodesPlacement NodesPlacementInfo `json:"nodesPlacementInfo,omitempty"` + // Nodes the redis cluster nodes + Nodes []core.RedisDetailedNode `json:"nodes,omitempty"` + // ClusterStatus the cluster status + ClusterStatus ClusterServiceStatus `json:"clusterStatus,omitempty"` + // Shards the cluster shards + Shards []*ClusterShards `json:"shards,omitempty"` +} + +// NewDistributedRedisClusterDetailedStatus create a new DistributedRedisClusterDetailedStatus +func NewDistributedRedisClusterDetailedStatus(status *DistributedRedisClusterStatus, nodes []core.RedisDetailedNode) *DistributedRedisClusterDetailedStatus { + ret := &DistributedRedisClusterDetailedStatus{ + Status: status.Status, + Reason: status.Reason, + NumberOfMaster: status.NumberOfMaster, + MinReplicationFactor: status.MinReplicationFactor, + MaxReplicationFactor: status.MaxReplicationFactor, + NodesPlacement: status.NodesPlacement, + ClusterStatus: status.ClusterStatus, + Shards: status.Shards, + Nodes: nodes, + } + for _, node := range status.Nodes { + ret.Nodes = append(ret.Nodes, core.RedisDetailedNode{RedisNode: node}) + } + return ret +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Shards",type="integer",JSONPath=".status.numberOfMaster",description="Current Shards" +// +kubebuilder:printcolumn:name="Service Status",type="string",JSONPath=".status.clusterStatus",description="Service status" +// +kubebuilder:printcolumn:name="Access",type="string",JSONPath=".spec.expose.type",description="Instance access type" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.status",description="Instance status" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.reason",description="Status message" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since creation" // DistributedRedisCluster is the Schema for the distributedredisclusters API type DistributedRedisCluster struct { diff --git a/api/redis.kun/v1alpha1/init.go b/api/cluster/v1alpha1/distributedrediscluster_webhook.go similarity index 67% rename from api/redis.kun/v1alpha1/init.go rename to api/cluster/v1alpha1/distributedrediscluster_webhook.go index ffcdf93..f004de7 100644 --- a/api/redis.kun/v1alpha1/init.go +++ b/api/cluster/v1alpha1/distributedrediscluster_webhook.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,32 +17,20 @@ limitations under the License. package v1alpha1 import ( - "fmt" - "net/netip" "os" - "github.com/alauda/redis-operator/pkg/config" + "github.com/alauda/redis-operator/api/core/helper" "github.com/alauda/redis-operator/pkg/types/redis" corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) -const ( - // Dumplicate with clusterbuilder - PrometheusExporterPort = 9100 - PrometheusExporterTelemetryPath = "/metrics" -) - -func (ins *DistributedRedisCluster) Init() error { +// Init +func (ins *DistributedRedisCluster) Default() error { + // added default forbid command if ins.Spec.Config == nil { ins.Spec.Config = map[string]string{} } - - if ins.Spec.Image == "" { - ins.Spec.Image = config.GetDefaultRedisImage() - } - ver, _ := redis.ParseRedisVersionFromImage(ins.Spec.Image) if ins.Spec.EnableTLS && !ver.IsTLSSupported() { ins.Spec.EnableTLS = false @@ -56,14 +44,23 @@ func (ins *DistributedRedisCluster) Init() error { ins.Spec.ServiceName = ins.Name } - if os.Getenv("POD_IP") != "" { - if operator_address, err := netip.ParseAddr(os.Getenv("POD_IP")); err == nil { - if operator_address.Is6() && ins.Spec.IPFamilyPrefer == "" { - ins.Spec.IPFamilyPrefer = v1.IPv6Protocol - } + if ins.Spec.Resources == nil || ins.Spec.Resources.Size() == 0 { + ins.Spec.Resources = &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1000m"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, } } + if ins.Spec.IPFamilyPrefer == "" { + ins.Spec.IPFamilyPrefer = helper.GetDefaultIPFamily(os.Getenv("POD_IP")) + } + if ins.Spec.Storage != nil && ins.Spec.Storage.Type == "" { ins.Spec.Storage.Type = PersistentClaim } @@ -85,12 +82,11 @@ func (ins *DistributedRedisCluster) Init() error { mon := ins.Spec.Monitor if mon != nil { - if mon.Image == "" { - mon.Image = config.GetDefaultExporterImage() + if mon.Prometheus == nil { + mon.Prometheus = &PrometheusSpec{} } - - if ins.Spec.Annotations == nil { - ins.Spec.Annotations = make(map[string]string) + if ins.Spec.PodAnnotations == nil { + ins.Spec.PodAnnotations = make(map[string]string) } if ins.Spec.Monitor.Resources == nil { @@ -105,9 +101,6 @@ func (ins *DistributedRedisCluster) Init() error { }, } } - ins.Spec.Annotations["prometheus.io/scrape"] = "true" - ins.Spec.Annotations["prometheus.io/path"] = PrometheusExporterTelemetryPath - ins.Spec.Annotations["prometheus.io/port"] = fmt.Sprintf("%d", PrometheusExporterPort) } return nil } diff --git a/api/redis.kun/v1alpha1/groupversion_info.go b/api/cluster/v1alpha1/groupversion_info.go similarity index 96% rename from api/redis.kun/v1alpha1/groupversion_info.go rename to api/cluster/v1alpha1/groupversion_info.go index 99dd5c7..ff205df 100644 --- a/api/redis.kun/v1alpha1/groupversion_info.go +++ b/api/cluster/v1alpha1/groupversion_info.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/api/redis.kun/v1alpha1/zz_generated.deepcopy.go b/api/cluster/v1alpha1/zz_generated.deepcopy.go similarity index 69% rename from api/redis.kun/v1alpha1/zz_generated.deepcopy.go rename to api/cluster/v1alpha1/zz_generated.deepcopy.go index 59b2f41..103311d 100644 --- a/api/redis.kun/v1alpha1/zz_generated.deepcopy.go +++ b/api/cluster/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2023 The RedisOperator Authors. @@ -8,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -22,45 +21,12 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/api/core/v1" + "github.com/alauda/redis-operator/api/core" + "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + corev1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackupSourceSpec) DeepCopyInto(out *BackupSourceSpec) { - *out = *in - if in.Args != nil { - in, out := &in.Args, &out.Args - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSourceSpec. -func (in *BackupSourceSpec) DeepCopy() *BackupSourceSpec { - if in == nil { - return nil - } - out := new(BackupSourceSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CheckerPolicy) DeepCopyInto(out *CheckerPolicy) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheckerPolicy. -func (in *CheckerPolicy) DeepCopy() *CheckerPolicy { - if in == nil { - return nil - } - out := new(CheckerPolicy) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterShardConfig) DeepCopyInto(out *ClusterShardConfig) { *out = *in @@ -122,27 +88,6 @@ func (in *ClusterShardsSlotStatus) DeepCopy() *ClusterShardsSlotStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DcOption) DeepCopyInto(out *DcOption) { - *out = *in - if in.InitADDRESS != nil { - in, out := &in.InitADDRESS, &out.InitADDRESS - *out = make([]RedisAddress, len(*in)) - copy(*out, *in) - } - out.CheckerPolicy = in.CheckerPolicy -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DcOption. -func (in *DcOption) DeepCopy() *DcOption { - if in == nil { - return nil - } - out := new(DcOption) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DistributedRedisCluster) DeepCopyInto(out *DistributedRedisCluster) { *out = *in @@ -170,6 +115,39 @@ func (in *DistributedRedisCluster) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DistributedRedisClusterDetailedStatus) DeepCopyInto(out *DistributedRedisClusterDetailedStatus) { + *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]core.RedisDetailedNode, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Shards != nil { + in, out := &in.Shards, &out.Shards + *out = make([]*ClusterShards, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ClusterShards) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DistributedRedisClusterDetailedStatus. +func (in *DistributedRedisClusterDetailedStatus) DeepCopy() *DistributedRedisClusterDetailedStatus { + if in == nil { + return nil + } + out := new(DistributedRedisClusterDetailedStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DistributedRedisClusterList) DeepCopyInto(out *DistributedRedisClusterList) { *out = *in @@ -207,7 +185,7 @@ func (in *DistributedRedisClusterSpec) DeepCopyInto(out *DistributedRedisCluster *out = *in if in.ImagePullSecrets != nil { in, out := &in.ImagePullSecrets, &out.ImagePullSecrets - *out = make([]v1.LocalObjectReference, len(*in)) + *out = make([]corev1.LocalObjectReference, len(*in)) copy(*out, *in) } if in.Command != nil { @@ -217,11 +195,16 @@ func (in *DistributedRedisClusterSpec) DeepCopyInto(out *DistributedRedisCluster } if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + *out = make([]corev1.EnvVar, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Shards != nil { + in, out := &in.Shards, &out.Shards + *out = make([]ClusterShardConfig, len(*in)) + copy(*out, *in) + } if in.Config != nil { in, out := &in.Config, &out.Config *out = make(map[string]string, len(*in)) @@ -229,14 +212,9 @@ func (in *DistributedRedisClusterSpec) DeepCopyInto(out *DistributedRedisCluster (*out)[key] = val } } - if in.Shards != nil { - in, out := &in.Shards, &out.Shards - *out = make([]ClusterShardConfig, len(*in)) - copy(*out, *in) - } if in.Affinity != nil { in, out := &in.Affinity, &out.Affinity - *out = new(v1.Affinity) + *out = new(corev1.Affinity) (*in).DeepCopyInto(*out) } if in.NodeSelector != nil { @@ -248,23 +226,23 @@ func (in *DistributedRedisClusterSpec) DeepCopyInto(out *DistributedRedisCluster } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations - *out = make([]v1.Toleration, len(*in)) + *out = make([]corev1.Toleration, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.SecurityContext != nil { in, out := &in.SecurityContext, &out.SecurityContext - *out = new(v1.PodSecurityContext) + *out = new(corev1.PodSecurityContext) (*in).DeepCopyInto(*out) } if in.ContainerSecurityContext != nil { in, out := &in.ContainerSecurityContext, &out.ContainerSecurityContext - *out = new(v1.SecurityContext) + *out = new(corev1.SecurityContext) (*in).DeepCopyInto(*out) } - if in.Annotations != nil { - in, out := &in.Annotations, &out.Annotations + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val @@ -277,12 +255,12 @@ func (in *DistributedRedisClusterSpec) DeepCopyInto(out *DistributedRedisCluster } if in.Resources != nil { in, out := &in.Resources, &out.Resources - *out = new(v1.ResourceRequirements) + *out = new(corev1.ResourceRequirements) (*in).DeepCopyInto(*out) } if in.PasswordSecret != nil { in, out := &in.PasswordSecret, &out.PasswordSecret - *out = new(v1.LocalObjectReference) + *out = new(corev1.LocalObjectReference) **out = **in } if in.Monitor != nil { @@ -290,10 +268,19 @@ func (in *DistributedRedisClusterSpec) DeepCopyInto(out *DistributedRedisCluster *out = new(Monitor) (*in).DeepCopyInto(*out) } - in.ServiceMonitor.DeepCopyInto(&out.ServiceMonitor) + if in.ServiceMonitor != nil { + in, out := &in.ServiceMonitor, &out.ServiceMonitor + *out = new(RedisServiceMonitorSpec) + (*in).DeepCopyInto(*out) + } in.Backup.DeepCopyInto(&out.Backup) out.Restore = in.Restore in.Expose.DeepCopyInto(&out.Expose) + if in.ServiceID != nil { + in, out := &in.ServiceID, &out.ServiceID + *out = new(int32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DistributedRedisClusterSpec. @@ -311,7 +298,7 @@ func (in *DistributedRedisClusterStatus) DeepCopyInto(out *DistributedRedisClust *out = *in if in.Nodes != nil { in, out := &in.Nodes, &out.Nodes - *out = make([]RedisClusterNode, len(*in)) + *out = make([]core.RedisNode, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -327,6 +314,11 @@ func (in *DistributedRedisClusterStatus) DeepCopyInto(out *DistributedRedisClust } } } + if in.DetailedStatusRef != nil { + in, out := &in.DetailedStatusRef, &out.DetailedStatusRef + *out = new(corev1.ObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DistributedRedisClusterStatus. @@ -342,6 +334,11 @@ func (in *DistributedRedisClusterStatus) DeepCopy() *DistributedRedisClusterStat // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Monitor) DeepCopyInto(out *Monitor) { *out = *in + if in.Prometheus != nil { + in, out := &in.Prometheus, &out.Prometheus + *out = new(PrometheusSpec) + (*in).DeepCopyInto(*out) + } if in.Args != nil { in, out := &in.Args, &out.Args *out = make([]string, len(*in)) @@ -349,19 +346,19 @@ func (in *Monitor) DeepCopyInto(out *Monitor) { } if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + *out = make([]corev1.EnvVar, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.Resources != nil { in, out := &in.Resources, &out.Resources - *out = new(v1.ResourceRequirements) + *out = new(corev1.ResourceRequirements) (*in).DeepCopyInto(*out) } if in.SecurityContext != nil { in, out := &in.SecurityContext, &out.SecurityContext - *out = new(v1.SecurityContext) + *out = new(corev1.SecurityContext) (*in).DeepCopyInto(*out) } } @@ -377,89 +374,49 @@ func (in *Monitor) DeepCopy() *Monitor { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisAddress) DeepCopyInto(out *RedisAddress) { +func (in *PrometheusSpec) DeepCopyInto(out *PrometheusSpec) { *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisAddress. -func (in *RedisAddress) DeepCopy() *RedisAddress { - if in == nil { - return nil - } - out := new(RedisAddress) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackup) DeepCopyInto(out *RedisBackup) { - *out = *in - if in.Schedule != nil { - in, out := &in.Schedule, &out.Schedule - *out = make([]Schedule, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackup. -func (in *RedisBackup) DeepCopy() *RedisBackup { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusSpec. +func (in *PrometheusSpec) DeepCopy() *PrometheusSpec { if in == nil { return nil } - out := new(RedisBackup) + out := new(PrometheusSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupStorage) DeepCopyInto(out *RedisBackupStorage) { +func (in *RedisServiceMonitorSpec) DeepCopyInto(out *RedisServiceMonitorSpec) { *out = *in - out.Size = in.Size.DeepCopy() -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupStorage. -func (in *RedisBackupStorage) DeepCopy() *RedisBackupStorage { - if in == nil { - return nil - } - out := new(RedisBackupStorage) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisClusterNode) DeepCopyInto(out *RedisClusterNode) { - *out = *in - if in.Slots != nil { - in, out := &in.Slots, &out.Slots - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisClusterNode. -func (in *RedisClusterNode) DeepCopy() *RedisClusterNode { - if in == nil { - return nil + if in.MetricRelabelConfigs != nil { + in, out := &in.MetricRelabelConfigs, &out.MetricRelabelConfigs + *out = make([]*v1.RelabelConfig, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(v1.RelabelConfig) + (*in).DeepCopyInto(*out) + } + } } - out := new(RedisClusterNode) - in.DeepCopyInto(out) - return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisRestore) DeepCopyInto(out *RedisRestore) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisRestore. -func (in *RedisRestore) DeepCopy() *RedisRestore { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisServiceMonitorSpec. +func (in *RedisServiceMonitorSpec) DeepCopy() *RedisServiceMonitorSpec { if in == nil { return nil } - out := new(RedisRestore) + out := new(RedisServiceMonitorSpec) in.DeepCopyInto(out) return out } @@ -479,20 +436,3 @@ func (in *RedisStorage) DeepCopy() *RedisStorage { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Schedule) DeepCopyInto(out *Schedule) { - *out = *in - in.Storage.DeepCopyInto(&out.Storage) - out.Target = in.Target -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Schedule. -func (in *Schedule) DeepCopy() *Schedule { - if in == nil { - return nil - } - out := new(Schedule) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/util/util.go b/api/core/helper/helper.go similarity index 50% rename from pkg/util/util.go rename to api/core/helper/helper.go index 74e39ce..dcfeced 100644 --- a/pkg/util/util.go +++ b/api/core/helper/helper.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,72 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package helper import ( - "crypto/rand" "fmt" + "net/netip" "sort" "strconv" "strings" -) - -func GenerateRedisTLSOptions() string { - return "--tls --cert /tls/tls.crt --key /tls/tls.key --cacert /tls/ca.crt" -} - -func GenerateRedisPasswordOptions() string { - return "--requirepass ${REDIS_PASSWORD} --masterauth ${REDIS_PASSWORD} --protected-mode yes " -} - -func GenerateRedisRandPassword() string { - buf := make([]byte, 32) - if _, err := rand.Read(buf); err != nil { - panic(err) - } - return fmt.Sprintf("%x", buf) -} - -func GenerateRedisRebuildAnnotation() map[string]string { - m := make(map[string]string) - m["middle.alauda.cn/rebuild"] = "true" - return m -} - -func GetEnvSentinelHost(name string) string { - return "RFS_REDIS_SERVICE_HOST" -} - -func GetEnvSentinelPort(name string) string { - return "RFS_REDIS_SERVICE_PORT_SENTINEL" -} -// ExtractLastNumber 提取字符串中最后一个"-"之后的数字 -func ExtractLastNumber(s string) int { - parts := strings.Split(s, "-") - lastPart := parts[len(parts)-1] - - num, err := strconv.Atoi(lastPart) - if err != nil { - return -1 - } - - return num -} - -// CompareStrings 比较两个字符串,返回 -1, 0, 1 -func CompareStrings(a, b string) int { - aNum, bNum := ExtractLastNumber(a), ExtractLastNumber(b) - - if aNum < bNum { - return -1 - } else if aNum > bNum { - return 1 - } - return 0 -} + v1 "k8s.io/api/core/v1" +) -func ParsePortSequence(portSequence string) ([]int32, error) { +// ParseSequencePorts +func ParseSequencePorts(portSequence string) ([]int32, error) { portRanges := strings.Split(portSequence, ",") portMap := make(map[int32]struct{}) @@ -87,7 +35,7 @@ func ParsePortSequence(portSequence string) ([]int32, error) { portRangeParts := strings.Split(portRange, "-") if len(portRangeParts) == 1 { - port, err := strconv.Atoi(portRangeParts[0]) + port, err := strconv.ParseInt(portRangeParts[0], 10, 32) if err != nil { return nil, err } @@ -118,3 +66,15 @@ func ParsePortSequence(portSequence string) ([]int32, error) { return ports, nil } + +func GetDefaultIPFamily(ip string) v1.IPFamily { + if ip == "" { + return "" + } + if addr, err := netip.ParseAddr(ip); err == nil { + if addr.Is6() { + return v1.IPv6Protocol + } + } + return "" +} diff --git a/api/core/helper/helper_test.go b/api/core/helper/helper_test.go new file mode 100644 index 0000000..7459219 --- /dev/null +++ b/api/core/helper/helper_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helper + +import ( + "fmt" + "testing" + + "gotest.tools/v3/assert" + v1 "k8s.io/api/core/v1" +) + +func TestParseSequencePorts(t *testing.T) { + testCases := []struct { + name string + portSequence string + expectedPorts []int32 + expectedError error + }{ + { + name: "Basic range", + portSequence: "1-3", + expectedPorts: []int32{1, 2, 3}, + expectedError: nil, + }, + { + name: "Single value", + portSequence: "5", + expectedPorts: []int32{5}, + expectedError: nil, + }, + { + name: "Mixed ranges and single values", + portSequence: "3,4-6,7,9-10", + expectedPorts: []int32{3, 4, 5, 6, 7, 9, 10}, + expectedError: nil, + }, + { + name: "Invalid format", + portSequence: "4-6,7-", + expectedPorts: nil, + expectedError: fmt.Errorf("strconv.Atoi: parsing \"\": invalid syntax"), + }, + { + name: "Unsorted and overlapping", + portSequence: "9-10,7,4-6,3,5-8", + expectedPorts: []int32{3, 4, 5, 6, 7, 8, 9, 10}, + expectedError: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ports, err := ParseSequencePorts(tc.portSequence) + + if tc.expectedError != nil && err == nil { + t.Errorf("Expected error, got nil") + } + + if tc.expectedError == nil && err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if tc.expectedError != nil && err != nil && tc.expectedError.Error() != err.Error() { + t.Errorf("Expected error '%v', got '%v'", tc.expectedError, err) + } + + if len(tc.expectedPorts) != len(ports) { + t.Errorf("Expected ports length %v, got %v", len(tc.expectedPorts), len(ports)) + } + + for i, port := range tc.expectedPorts { + if port != ports[i] { + t.Errorf("Expected port %v at position %v, got %v", port, i, ports[i]) + } + } + }) + } +} + +func TestGetDefaultIPFamily(t *testing.T) { + tests := []struct { + name string + ip string + expected v1.IPFamily + }{ + { + name: "Empty IP", + ip: "", + expected: "", + }, + { + name: "Valid IPv6", + ip: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + expected: v1.IPv6Protocol, + }, + { + name: "Valid IPv6", + ip: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + expected: v1.IPv6Protocol, + }, + { + name: "Invalid IP", + ip: "invalid-ip", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetDefaultIPFamily(tt.ip) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/api/core/types.go b/api/core/types.go new file mode 100644 index 0000000..3270f73 --- /dev/null +++ b/api/core/types.go @@ -0,0 +1,179 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package core + +// +kubebuilder:object:generate=true + +import ( + "net" + "strconv" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +type Arch string + +const ( + // RedisCluster is the Redis Cluster arch + RedisCluster Arch = "cluster" + // RedisSentinel is the Redis Sentinel arch, which should be renamed to Failover + RedisSentinel Arch = "sentinel" + // RedisStandalone is the Redis Standalone arch + RedisStandalone Arch = "standalone" + // RedisStdSentinel is the Redis Standard Sentinel arch + RedisStdSentinel Arch = "stdsentinel" +) + +// RedisRole redis node role type +type RedisRole string + +const ( + // RedisRoleMaster Master node role + RedisRoleMaster RedisRole = "Master" + // RedisRoleReplica Master node role + RedisRoleReplica RedisRole = "Slave" + // RedisRoleSentinel Master node role + RedisRoleSentinel RedisRole = "Sentinel" + // RedisRoleNone None node role + RedisRoleNone RedisRole = "None" +) + +// AffinityPolicy +type AffinityPolicy string + +const ( + SoftAntiAffinity AffinityPolicy = "SoftAntiAffinity" + AntiAffinityInSharding AffinityPolicy = "AntiAffinityInSharding" + AntiAffinity AffinityPolicy = "AntiAffinity" +) + +// InstanceAccessBase +type InstanceAccessBase struct { + // ServiceType defines the type of the all related service + // +kubebuilder:validation:Enum=NodePort;LoadBalancer;ClusterIP + ServiceType corev1.ServiceType `json:"type,omitempty"` + + // The annnotations of the service which attached to services + Annotations map[string]string `json:"annotations,omitempty"` + // AccessPort defines the lb access nodeport + AccessPort int32 `json:"accessPort,omitempty"` + // NodePortMap defines the map of the nodeport for redis nodes + // NodePortSequence defines the sequence of the nodeport for redis cluster only + NodePortSequence string `json:"dataStorageNodePortSequence,omitempty"` + // NodePortMap defines the map of the nodeport for redis sentinel only + // TODO: deprecated this field with NodePortSequence in 3.22 + // Reversed for 3.14 backward compatibility + NodePortMap map[string]int32 `json:"dataStorageNodePortMap,omitempty"` +} + +// InstanceAccess +type InstanceAccess struct { + InstanceAccessBase `json:",inline"` + + // IPFamily represents the IP Family (IPv4 or IPv6). This type is used to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). + // +kubebuilder:validation:Enum=IPv4;IPv6 + IPFamilyPrefer corev1.IPFamily `json:"ipFamilyPrefer,omitempty"` + + // Image defines the image used to expose redis from annotations + Image string `json:"image,omitempty"` + // ImagePullPolicy defines the image pull policy + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` +} + +// RedisNode represent a RedisCluster Node +type RedisNode struct { + // ID is the redis cluster node id, not runid + ID string `json:"id,omitempty"` + // Role is the role of the node, master or slave + Role RedisRole `json:"role"` + // IP is the ip of the node. if access announce is enabled, it will be the access ip + IP string `json:"ip"` + // Port is the port of the node. if access announce is enabled, it will be the access port + Port string `json:"port"` + // Slots is the slot range for the shard, eg: 0-1000,1002,1005-1100 + Slots []string `json:"slots,omitempty"` + // MasterRef is the master node id of this node + MasterRef string `json:"masterRef,omitempty"` + // PodName current pod name + PodName string `json:"podName"` + // NodeName is the node name of the node where holds the pod + NodeName string `json:"nodeName"` + // StatefulSet is the statefulset name of this pod + StatefulSet string `json:"statefulSet"` +} + +// RedisDetailedNode represent a redis Node with more details +type RedisDetailedNode struct { + RedisNode + // Version version of redis + Version string `json:"version,omitempty"` + // UsedMemory + UsedMemory int64 `json:"usedMemory,omitempty"` + // UsedMemoryDataset + UsedMemoryDataset int64 `json:"usedMemoryDataset,omitempty"` +} + +// RedisBackup defines the structure used to backup the Redis Data +type RedisBackup struct { + Image string `json:"image,omitempty"` + Schedule []Schedule `json:"schedule,omitempty"` +} + +type Schedule struct { + Name string `json:"name,omitempty"` + Schedule string `json:"schedule"` + Keep int32 `json:"keep"` + KeepAfterDeletion bool `json:"keepAfterDeletion,omitempty"` + Storage RedisBackupStorage `json:"storage"` + Target RedisBackupTarget `json:"target,omitempty"` +} + +type RedisBackupTarget struct { + // S3Option + S3Option S3Option `json:"s3Option,omitempty"` +} + +type S3Option struct { + S3Secret string `json:"s3Secret,omitempty"` + Bucket string `json:"bucket,omitempty"` + Dir string `json:"dir,omitempty"` +} + +type RedisBackupStorage struct { + StorageClassName string `json:"storageClassName,omitempty"` + Size resource.Quantity `json:"size,omitempty"` +} + +// RedisBackup defines the structure used to restore the Redis Data +type RedisRestore struct { + Image string `json:"image,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + BackupName string `json:"backupName,omitempty"` +} + +// HostPort +type HostPort struct { + // Host the sentinel host + Host string `json:"host,omitempty"` + // Port the sentinel port + Port int32 `json:"port,omitempty"` +} + +func (hp *HostPort) String() string { + return net.JoinHostPort(hp.Host, strconv.Itoa(int(hp.Port))) +} diff --git a/api/core/validation/validation.go b/api/core/validation/validation.go new file mode 100644 index 0000000..739c1f8 --- /dev/null +++ b/api/core/validation/validation.go @@ -0,0 +1,55 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validation + +import ( + "fmt" + "slices" + + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// ValidateInstanceAccess validates the instance access. +func ValidateInstanceAccess(acc *core.InstanceAccessBase, nodeCount int, warns *admission.Warnings) error { + if acc == nil || acc.ServiceType == "" { + return nil + } + + if !slices.Contains([]corev1.ServiceType{ + corev1.ServiceTypeClusterIP, + corev1.ServiceTypeNodePort, + corev1.ServiceTypeLoadBalancer, + }, acc.ServiceType) { + return fmt.Errorf("unsupported service type: %s", acc.ServiceType) + } + + if acc.ServiceType == corev1.ServiceTypeNodePort { + if acc.NodePortSequence != "" { + ports, err := helper.ParseSequencePorts(acc.NodePortSequence) + if err != nil { + return fmt.Errorf("failed to parse data storage node port sequence: %v", err) + } + if nodeCount != len(ports) { + return fmt.Errorf("expected %d nodes, but got %d ports in node port sequence", nodeCount, len(ports)) + } + } + } + return nil +} diff --git a/api/core/validation/validation_test.go b/api/core/validation/validation_test.go new file mode 100644 index 0000000..b80bfe6 --- /dev/null +++ b/api/core/validation/validation_test.go @@ -0,0 +1,114 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validation + +import ( + "testing" + + "github.com/alauda/redis-operator/api/core" + v1 "k8s.io/api/core/v1" +) + +func TestValidateInstanceAccess(t *testing.T) { + type args struct { + acc *core.InstanceAccess + nc int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "service type not specified", + args: args{ + acc: &core.InstanceAccess{}, + }, + }, + { + name: "service type is ClusterIP", + args: args{ + acc: &core.InstanceAccess{InstanceAccessBase: core.InstanceAccessBase{ServiceType: v1.ServiceTypeClusterIP}}, + }, + }, + { + name: "service type is LoadBalancer", + args: args{ + acc: &core.InstanceAccess{InstanceAccessBase: core.InstanceAccessBase{ServiceType: v1.ServiceTypeLoadBalancer}}, + }, + }, + { + name: "service type is ExternalName", + args: args{ + acc: &core.InstanceAccess{InstanceAccessBase: core.InstanceAccessBase{ServiceType: v1.ServiceTypeExternalName}}, + }, + wantErr: true, + }, + { + name: "nodeport without specified ports", + args: args{ + acc: &core.InstanceAccess{InstanceAccessBase: core.InstanceAccessBase{ServiceType: v1.ServiceTypeNodePort}}, + }, + wantErr: false, + }, + { + name: "nodeport without specified sequence ports", + args: args{ + acc: &core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: v1.ServiceTypeNodePort, + NodePortSequence: "6379,6380,6381", + }, + }, + }, + wantErr: true, + }, + { + name: "nodeport with matched sequence ports", + args: args{ + acc: &core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: v1.ServiceTypeNodePort, + NodePortSequence: "6379,6380,6381", + }, + }, + nc: 3, + }, + wantErr: false, + }, + { + name: "nodeport with not matched sequence ports", + args: args{ + acc: &core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: v1.ServiceTypeNodePort, + NodePortSequence: "6379,6380,6381", + }, + }, + nc: 5, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateInstanceAccess(&tt.args.acc.InstanceAccessBase, tt.args.nc, nil); (err != nil) != tt.wantErr { + t.Errorf("ValidateInstanceAccess() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/api/core/zz_generated.deepcopy.go b/api/core/zz_generated.deepcopy.go new file mode 100644 index 0000000..ec0bfe8 --- /dev/null +++ b/api/core/zz_generated.deepcopy.go @@ -0,0 +1,220 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package core + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostPort) DeepCopyInto(out *HostPort) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPort. +func (in *HostPort) DeepCopy() *HostPort { + if in == nil { + return nil + } + out := new(HostPort) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceAccess) DeepCopyInto(out *InstanceAccess) { + *out = *in + in.InstanceAccessBase.DeepCopyInto(&out.InstanceAccessBase) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceAccess. +func (in *InstanceAccess) DeepCopy() *InstanceAccess { + if in == nil { + return nil + } + out := new(InstanceAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceAccessBase) DeepCopyInto(out *InstanceAccessBase) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.NodePortMap != nil { + in, out := &in.NodePortMap, &out.NodePortMap + *out = make(map[string]int32, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceAccessBase. +func (in *InstanceAccessBase) DeepCopy() *InstanceAccessBase { + if in == nil { + return nil + } + out := new(InstanceAccessBase) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisBackup) DeepCopyInto(out *RedisBackup) { + *out = *in + if in.Schedule != nil { + in, out := &in.Schedule, &out.Schedule + *out = make([]Schedule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackup. +func (in *RedisBackup) DeepCopy() *RedisBackup { + if in == nil { + return nil + } + out := new(RedisBackup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisBackupStorage) DeepCopyInto(out *RedisBackupStorage) { + *out = *in + out.Size = in.Size.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupStorage. +func (in *RedisBackupStorage) DeepCopy() *RedisBackupStorage { + if in == nil { + return nil + } + out := new(RedisBackupStorage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisBackupTarget) DeepCopyInto(out *RedisBackupTarget) { + *out = *in + out.S3Option = in.S3Option +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupTarget. +func (in *RedisBackupTarget) DeepCopy() *RedisBackupTarget { + if in == nil { + return nil + } + out := new(RedisBackupTarget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisDetailedNode) DeepCopyInto(out *RedisDetailedNode) { + *out = *in + in.RedisNode.DeepCopyInto(&out.RedisNode) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisDetailedNode. +func (in *RedisDetailedNode) DeepCopy() *RedisDetailedNode { + if in == nil { + return nil + } + out := new(RedisDetailedNode) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisNode) DeepCopyInto(out *RedisNode) { + *out = *in + if in.Slots != nil { + in, out := &in.Slots, &out.Slots + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisNode. +func (in *RedisNode) DeepCopy() *RedisNode { + if in == nil { + return nil + } + out := new(RedisNode) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisRestore) DeepCopyInto(out *RedisRestore) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisRestore. +func (in *RedisRestore) DeepCopy() *RedisRestore { + if in == nil { + return nil + } + out := new(RedisRestore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3Option) DeepCopyInto(out *S3Option) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Option. +func (in *S3Option) DeepCopy() *S3Option { + if in == nil { + return nil + } + out := new(S3Option) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Schedule) DeepCopyInto(out *Schedule) { + *out = *in + in.Storage.DeepCopyInto(&out.Storage) + out.Target = in.Target +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Schedule. +func (in *Schedule) DeepCopy() *Schedule { + if in == nil { + return nil + } + out := new(Schedule) + in.DeepCopyInto(out) + return out +} diff --git a/api/databases.spotahome.com/v1/redisfailover_types.go b/api/databases.spotahome.com/v1/redisfailover_types.go deleted file mode 100644 index c0531b3..0000000 --- a/api/databases.spotahome.com/v1/redisfailover_types.go +++ /dev/null @@ -1,334 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1 - -import ( - smv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// RedisFailoverSpec represents a Redis failover spec -type RedisFailoverSpec struct { - // Redis redis data node settings - Redis RedisSettings `json:"redis,omitempty"` - // Sentinel sentinel node settings - Sentinel SentinelSettings `json:"sentinel,omitempty"` - // Auth default user auth settings - Auth AuthSettings `json:"auth,omitempty"` - // LabelWhitelist is a list of label names that are allowed to be present on a pod - LabelWhitelist []string `json:"labelWhitelist,omitempty"` - // EnableTLS enable TLS for Redis - EnableTLS bool `json:"enableTLS,omitempty"` - // ServiceMonitor service monitor for prometheus - ServiceMonitor RedisServiceMonitorSpec `json:"serviceMonitor,omitempty"` - // Expose service access configuration - Expose RedisExpose `json:"expose,omitempty"` -} - -// RedisServiceMonitorSpec -type RedisServiceMonitorSpec struct { - // CustomMetricRelabelings custom metric relabelings - CustomMetricRelabelings bool `json:"customMetricRelabelings,omitempty"` - // MetricRelabelConfigs metric relabel configs - MetricRelabelConfigs []*smv1.RelabelConfig `json:"metricRelabelings,omitempty"` - // Interval - Interval string `json:"interval,omitempty"` - // ScrapeTimeout - ScrapeTimeout string `json:"scrapeTimeout,omitempty"` -} - -// RedisExpose for nodeport -type RedisExpose struct { - // EnableNodePort enable nodeport - EnableNodePort bool `json:"enableNodePort,omitempty"` - // ExposeImage expose image - ExposeImage string `json:"exposeImage,omitempty"` - // AccessPort lb service access port - AccessPort int32 `json:"accessPort,omitempty"` - // DataStorageNodePortSequence redis port list separated by commas - DataStorageNodePortSequence string `json:"dataStorageNodePortSequence,omitempty"` - // DataStorageNodePortMap redis port map referred by pod name - DataStorageNodePortMap map[string]int32 `json:"dataStorageNodePortMap,omitempty"` -} - -// RedisSettings defines the specification of the redis cluster -type RedisSettings struct { - // Image is the Redis image to run. - Image string `json:"image,omitempty"` - // ImagePullPolicy is the Image pull policy. - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` - // Replicas is the number of Redis replicas to run. - // +kubebuilder:validation:Minimum=0 - // +kubebuilder:validation:Maximum=5 - Replicas int32 `json:"replicas,omitempty"` - // Resources is the resource requirements for the Redis container. - Resources corev1.ResourceRequirements `json:"resources,omitempty"` - // CustomConfig is a map of custom Redis configuration options. - CustomConfig map[string]string `json:"customConfig,omitempty"` - // Storage redis data persistence settings. - Storage RedisStorage `json:"storage,omitempty"` - // Exporter prometheus exporter settings. - Exporter RedisExporter `json:"exporter,omitempty"` - // Affinity is the affinity settings for the Redis pods. - Affinity *corev1.Affinity `json:"affinity,omitempty"` - // SecurityContext is the security context for the Redis pods. - SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` - // ImagePullSecrets is the list of secrets used to pull the Redis image from a private registry. - ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` - // Tolerations is the list of tolerations for the Redis pods. - Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - // NodeSelector is the node selector for the Redis pods. - NodeSelector map[string]string `json:"nodeSelector,omitempty"` - // PodAnnotations is the annotations for the Redis pods. - PodAnnotations map[string]string `json:"podAnnotations,omitempty"` - // ServiceAnnotations is the annotations for the Redis service. - ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"` - // HostNetwork is the host network settings for the Redis pods. - HostNetwork bool `json:"hostNetwork,omitempty"` - // DNSPolicy is the DNS policy for the Redis pods. - DNSPolicy corev1.DNSPolicy `json:"dnsPolicy,omitempty"` - // Backup schedule backup configuration. - Backup RedisBackup `json:"backup,omitempty"` - // Restore restore redis instance from backup. - Restore RedisRestore `json:"restore,omitempty"` - // IPFamily represents the IP Family (IPv4 or IPv6). This type is used to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). - // +kubebuilder:validation:Enum=IPv4;IPv6 - IPFamilyPrefer corev1.IPFamily `json:"ipFamilyPrefer,omitempty"` -} - -// SentinelSettings defines the specification of the sentinel cluster -type SentinelSettings struct { - // Image is the Redis image to run. - Image string `json:"image,omitempty"` - // ImagePullPolicy is the Image pull policy. - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` - // Replicas is the number of Redis replicas to run. - // +kubebuilder:validation:Minimum=3 - Replicas int32 `json:"replicas,omitempty"` - // Resources is the resource requirements for the Redis container. - Resources corev1.ResourceRequirements `json:"resources,omitempty"` - // CustomConfig sentinel configuration options. - CustomConfig map[string]string `json:"customConfig,omitempty"` - // Command startup commands - Command []string `json:"command,omitempty"` - // Affinity is the affinity settings for the Redis pods. - Affinity *corev1.Affinity `json:"affinity,omitempty"` - // SecurityContext is the security context for the Redis pods. - SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` - // ImagePullSecrets is the list of secrets used to pull the Redis image from a private registry. - ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` - // Tolerations is the list of tolerations for the Redis pods. - Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - // NodeSelector is the node selector for the Redis pods. - NodeSelector map[string]string `json:"nodeSelector,omitempty"` - // PodAnnotations is the annotations for the Redis pods. - PodAnnotations map[string]string `json:"podAnnotations,omitempty"` - // ServiceAnnotations is the annotations for the Redis service. - ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"` - // Exporter prometheus exporter settings. - Exporter SentinelExporter `json:"exporter,omitempty"` - // HostNetwork is the host network settings for the Redis pods. - HostNetwork bool `json:"hostNetwork,omitempty"` - // DNSPolicy is the DNS policy for the Redis pods. - DNSPolicy corev1.DNSPolicy `json:"dnsPolicy,omitempty"` -} - -// AuthSettings contains settings about auth -type AuthSettings struct { - // SecretName is the name of the secret containing the auth credentials. - SecretPath string `json:"secretPath,omitempty"` -} - -// RedisExporter defines the specification for the redis exporter -type RedisExporter struct { - // Enabled is the flag to enable redis exporter - Enabled bool `json:"enabled,omitempty"` - // Image exporter image - Image string `json:"image,omitempty"` - // ImagePullPolicy image pull policy. - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` -} - -// SentinelExporter defines the specification for the sentinel exporter -type SentinelExporter struct { - // Enabled is the flag to enable sentinel exporter - Enabled bool `json:"enabled,omitempty"` - // Image exporter image - Image string `json:"image,omitempty"` - // ImagePullPolicy image pull policy. - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` -} - -// RedisStorage defines the structure used to store the Redis Data -type RedisStorage struct { - // KeepAfterDeletion is the flag to keep the data after the RedisFailover is deleted. - KeepAfterDeletion bool `json:"keepAfterDeletion,omitempty"` - // PersistentVolumeClaim is the PVC volume source. - PersistentVolumeClaim *corev1.PersistentVolumeClaim `json:"persistentVolumeClaim,omitempty"` -} - -// RedisBackup defines the structure used to backup the Redis Data -type RedisBackup struct { - // Image is the Redis backup image to run. - Image string `json:"image,omitempty"` - // Schedule is the backup schedule. - Schedule []Schedule `json:"schedule,omitempty"` -} - -// Schedule -type Schedule struct { - // Name is the scheduled backup name. - Name string `json:"name,omitempty"` - // Schedule crontab like schedule. - Schedule string `json:"schedule"` - // Keep is the number of backups to keep. - // +kubebuilder:validation:Minimum=1 - Keep int32 `json:"keep"` - // KeepAfterDeletion is the flag to keep the data after the RedisFailover is deleted. - KeepAfterDeletion bool `json:"keepAfterDeletion,omitempty"` - // Storage is the backup storage configuration. - Storage RedisBackupStorage `json:"storage"` - // Target is the backup target configuration. - Target RedisBackupTarget `json:"target,omitempty"` -} - -// RedisBackupTarget -type RedisBackupTarget struct { - // S3Option is the S3 backup target configuration. - S3Option S3Option `json:"s3Option,omitempty"` -} - -// S3Option -type S3Option struct { - // S3Secret s3 storage access secret - S3Secret string `json:"s3Secret,omitempty"` - // Bucket s3 storage bucket - Bucket string `json:"bucket,omitempty"` - // Dir s3 storage dir - Dir string `json:"dir,omitempty"` -} - -// RedisBackupStorage -type RedisBackupStorage struct { - // StorageClassName is the name of the StorageClass to use for the PersistentVolumeClaim. - StorageClassName string `json:"storageClassName,omitempty"` - // Size is the size of the PersistentVolumeClaim. - Size resource.Quantity `json:"size,omitempty"` -} - -// RedisBackup defines the structure used to restore the Redis Data -type RedisRestore struct { - // Image is the Redis restore image to run. - Image string `json:"image,omitempty"` - // ImagePullPolicy is the Image pull policy. - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` - // BackupName is the backup cr name to restore. - BackupName string `json:"backupName,omitempty"` -} - -// Phase -type Phase string - -// RedisFailoverStatus defines the observed state of RedisFailover -type RedisFailoverStatus struct { - // The last time this condition was updated. - LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` - // Last time the condition transitioned from one status to another. - LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` - // Message - Message string `json:"message,omitempty"` - // Creating, Pending, Fail, Ready - Phase Phase `json:"phase,omitempty"` - - // Instance - Instance RedisStatusInstance `json:"instance,omitempty"` - // Master master status - Master RedisStatusMaster `json:"master,omitempty"` - // Version - Version string `json:"version,omitempty"` -} - -// RedisStatusInstance -type RedisStatusInstance struct { - Redis RedisStatusInstanceRedis `json:"redis,omitempty"` - Sentinel RedisStatusInstanceSentinel `json:"sentinel,omitempty"` -} - -// RedisStatusInstanceRedis -type RedisStatusInstanceRedis struct { - Size int32 `json:"size,omitempty"` - Ready int32 `json:"ready,omitempty"` -} - -// RedisStatusInstanceSentinel -type RedisStatusInstanceSentinel struct { - Size int32 `json:"size,omitempty"` - Ready int32 `json:"ready,omitempty"` - Service string `json:"service,omitempty"` - ClusterIP string `json:"clusterIp,omitempty"` - Port string `json:"port,omitempty"` -} - -// RedisStatusMaster -type RedisStatusMaster struct { - // Name master pod name - Name string `json:"name"` - // Status master service status - Status RedisStatusMasterStatus `json:"status"` - // Address master access ip:port - Address string `json:"address"` -} - -type RedisStatusMasterStatus string - -const ( - // RedisStatusMasterOK master is online - RedisStatusMasterOK RedisStatusMasterStatus = "ok" - // RedisStatusMasterDown master is offline - RedisStatusMasterDown RedisStatusMasterStatus = "down" -) - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:printcolumn:name="Master",type="string",JSONPath=".status.master.address",description="Master address" -//+kubebuilder:printcolumn:name="Master Status",type="string",JSONPath=".status.master.status",description="Master status" -//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Instance reconcile phase" -//+kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.reason",description="Status message" - -// RedisFailover is the Schema for the redisfailovers API -type RedisFailover struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec RedisFailoverSpec `json:"spec,omitempty"` - Status RedisFailoverStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// RedisFailoverList contains a list of RedisFailover -type RedisFailoverList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []RedisFailover `json:"items"` -} - -func init() { - SchemeBuilder.Register(&RedisFailover{}, &RedisFailoverList{}) -} diff --git a/api/databases.spotahome.com/v1/validate.go b/api/databases.spotahome.com/v1/validate.go deleted file mode 100644 index 899fc23..0000000 --- a/api/databases.spotahome.com/v1/validate.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1 - -import ( - "fmt" - "net/netip" - "os" - - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/types/redis" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -const ( - maxNameLength = 48 - defaultRedisNumber = 2 - DefaultSentinelNumber = 3 -) - -var ( - defaultExporterImage = config.GetDefaultExporterImage() - defaultImage = config.GetDefaultRedisImage() -) - -// Validate set the values by default if not defined and checks if the values given are valid -func (r *RedisFailover) Validate() error { - if len(r.Name) > maxNameLength { - return fmt.Errorf("name length can't be higher than %d", maxNameLength) - } - - if r.Spec.Redis.Image == "" { - r.Spec.Redis.Image = defaultImage - } - - if r.Spec.Sentinel.Image == "" { - r.Spec.Sentinel.Image = defaultImage - } - - if r.Spec.Redis.Replicas <= 0 { - r.Spec.Redis.Replicas = defaultRedisNumber - } - - if r.Spec.Sentinel.Replicas <= 0 { - r.Spec.Sentinel.Replicas = DefaultSentinelNumber - } - - if r.Spec.Redis.Exporter.Image == "" { - r.Spec.Redis.Exporter.Image = defaultExporterImage - } - - // if r.Spec.Sentinel.Exporter.Image == "" { - // r.Spec.Sentinel.Exporter.Image = defaultSentinelExporterImage - // } - - v, err := redis.ParseRedisVersionFromImage(r.Spec.Redis.Image) - if err != nil { - return err - } - // tls only support redis >= 6.0 - if r.Spec.EnableTLS && !v.IsTLSSupported() { - r.Spec.EnableTLS = false - } - - if r.Spec.Redis.Resources.Size() == 0 { - r.Spec.Redis.Resources = defaultRedisResource() - } - - if r.Spec.Sentinel.Resources.Size() == 0 { - r.Spec.Sentinel.Resources = defaultSentinelResource() - } - if os.Getenv("POD_IP") != "" { - if operator_address, err := netip.ParseAddr(os.Getenv("POD_IP")); err == nil { - if operator_address.Is6() && r.Spec.Redis.IPFamilyPrefer == "" { - r.Spec.Redis.IPFamilyPrefer = v1.IPv6Protocol - } - } - } - return nil -} - -func defaultRedisResource() v1.ResourceRequirements { - return v1.ResourceRequirements{ - Requests: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("200m"), - v1.ResourceMemory: resource.MustParse("256Mi"), - }, - Limits: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("1000m"), - v1.ResourceMemory: resource.MustParse("2Gi"), - }, - } -} - -func defaultSentinelResource() v1.ResourceRequirements { - return v1.ResourceRequirements{ - Requests: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("100m"), - v1.ResourceMemory: resource.MustParse("128Mi"), - }, - Limits: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("200m"), - v1.ResourceMemory: resource.MustParse("256Mi"), - }, - } -} diff --git a/api/databases.spotahome.com/v1/groupversion_info.go b/api/databases/v1/groupversion_info.go similarity index 96% rename from api/databases.spotahome.com/v1/groupversion_info.go rename to api/databases/v1/groupversion_info.go index fe0e727..448982c 100644 --- a/api/databases.spotahome.com/v1/groupversion_info.go +++ b/api/databases/v1/groupversion_info.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/api/databases/v1/redisfailover_types.go b/api/databases/v1/redisfailover_types.go new file mode 100644 index 0000000..d4acf66 --- /dev/null +++ b/api/databases/v1/redisfailover_types.go @@ -0,0 +1,282 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/alauda/redis-operator/api/core" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RedisFailoverStatus defines the observed state of RedisFailover + +const ( + DefaultSentinelNumber = 3 +) + +// RedisFailoverSpec represents a Redis failover spec +type RedisFailoverSpec struct { + Redis RedisSettings `json:"redis,omitempty"` + Sentinel *SentinelSettings `json:"sentinel,omitempty"` + // Auth + Auth AuthSettings `json:"auth,omitempty"` + + // TODO: remove this field in 3.20, which not used + // +kubebuilder:deprecatedversion:warning=not supported anymore + LabelWhitelist []string `json:"labelWhitelist,omitempty"` + + // EnableActiveRedis enable active-active model for Redis + EnableActiveRedis bool `json:"enableActiveRedis,omitempty"` + // ServiceID the service id for activeredis + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=15 + ServiceID *int32 `json:"serviceID,omitempty"` +} + +// RedisSettings defines the specification of the redis cluster +type RedisSettings struct { + Image string `json:"image,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + // CustomConfig custom redis configuration + CustomConfig map[string]string `json:"customConfig,omitempty"` + Storage RedisStorage `json:"storage,omitempty"` + + // Exporter + Exporter RedisExporter `json:"exporter,omitempty"` + // Expose + Expose core.InstanceAccess `json:"expose,omitempty"` + // EnableTLS enable TLS for Redis + EnableTLS bool `json:"enableTLS,omitempty"` + + Affinity *corev1.Affinity `json:"affinity,omitempty"` + SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + PodAnnotations map[string]string `json:"podAnnotations,omitempty"` + ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"` + + Backup core.RedisBackup `json:"backup,omitempty"` + Restore core.RedisRestore `json:"restore,omitempty"` +} + +// Authorization defines the authorization settings for redis +type Authorization struct { + // Username the username for redis + Username string `json:"username,omitempty"` + // PasswordSecret the password secret for redis + PasswordSecret string `json:"passwordSecret,omitempty"` + // TLSSecret the tls secret + TLSSecret string `json:"tlsSecret,omitempty"` +} + +// SentinelReference defines the sentinel reference +type SentinelReference struct { + // Addresses the sentinel addresses + // +kubebuilder:validation:MinItems=3 + Nodes []SentinelMonitorNode `json:"nodes,omitempty"` + // Auth the sentinel auth + Auth Authorization `json:"auth,omitempty"` +} + +// SentinelSettings defines the specification of the sentinel cluster +type SentinelSettings struct { + RedisSentinelSpec `json:",inline"` + // SentinelReference the sentinel reference + SentinelReference *SentinelReference `json:"sentinelReference,omitempty"` + // MonitorConfig configs for sentinel to monitor this replication, including: + // - down-after-milliseconds + // - failover-timeout + // - parallel-syncs + MonitorConfig map[string]string `json:"monitorConfig,omitempty"` + // Quorum the number of Sentinels that need to agree about the fact the master is not reachable, + // in order to really mark the master as failing, and eventually start a failover procedure if possible. + // If not specified, the default value is the majority of the Sentinels. + Quorum *int32 `json:"quorum,omitempty"` +} + +// AuthSettings contains settings about auth +type AuthSettings struct { + SecretPath string `json:"secretPath,omitempty"` +} + +// RedisExporter defines the specification for the redis exporter +type RedisExporter struct { + Enabled bool `json:"enabled,omitempty"` + Image string `json:"image,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + +// SentinelExporter defines the specification for the sentinel exporter +type SentinelExporter struct { + Enabled bool `json:"enabled,omitempty"` + Image string `json:"image,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` +} + +// RedisStorage defines the structure used to store the Redis Data +type RedisStorage struct { + KeepAfterDeletion bool `json:"keepAfterDeletion,omitempty"` + EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"` + PersistentVolumeClaim *corev1.PersistentVolumeClaim `json:"persistentVolumeClaim,omitempty"` +} + +type Phase string + +const ( + Fail Phase = "Fail" + Creating Phase = "Creating" + Pending Phase = "Pending" + Ready Phase = "Ready" + WaitingPodReady Phase = "WaitingPodReady" + Paused Phase = "Paused" +) + +type FailoverPolicy string + +const ( + SentinelFailoverPolicy FailoverPolicy = "sentinel" + ManualFailoverPolicy FailoverPolicy = "manual" +) + +type SentinelMonitorNode struct { + // IP the sentinel node ip + IP string `json:"ip,omitempty"` + // Port the sentinel node port + Port int32 `json:"port,omitempty"` + // Flags + Flags string `json:"flags,omitempty"` +} + +type MonitorStatus struct { + // Policy the failover policy + Policy FailoverPolicy `json:"policy,omitempty"` + // Name monitor name + Name string `json:"name,omitempty"` + // Username sentinel username + Username string `json:"username,omitempty"` + // PasswordSecret + PasswordSecret string `json:"passwordSecret,omitempty"` + // OldPasswordSecret + OldPasswordSecret string `json:"oldPasswordSecret,omitempty"` + // TLSSecret the tls secret + TLSSecret string `json:"tlsSecret,omitempty"` + // Nodes the sentinel monitor nodes + Nodes []SentinelMonitorNode `json:"nodes,omitempty"` +} + +// RedisFailoverStatus +type RedisFailoverStatus struct { + // Phase + Phase Phase `json:"phase,omitempty"` + // Message the status message + Message string `json:"message,omitempty"` + // Instance the redis instance replica info + Instance RedisStatusInstance `json:"instance,omitempty"` + // Master the redis master access info + Master RedisStatusMaster `json:"master,omitempty"` + // Nodes the redis cluster nodes + Nodes []core.RedisNode `json:"nodes,omitempty"` + // Version + // TODO: remove this field in 3.20, which not used + Version string `json:"version,omitempty"` + // TLSSecret the tls secret + TLSSecret string `json:"tlsSecret,omitempty"` + // Monitor the monitor status + Monitor MonitorStatus `json:"monitor,omitempty"` + + // DetailedStatusRef detailed status resource ref + DetailedStatusRef *corev1.ObjectReference `json:"detailedStatusRef,omitempty"` +} + +// RedisFailoverDetailedStatus +type RedisFailoverDetailedStatus struct { + // Phase + Phase Phase `json:"phase,omitempty"` + // Message the status message + Message string `json:"message,omitempty"` + // Nodes the redis cluster nodes + Nodes []core.RedisDetailedNode `json:"nodes,omitempty"` +} + +type RedisStatusInstance struct { + Redis RedisStatusInstanceRedis `json:"redis,omitempty"` + // Sentinel the sentinel instance info + // +kubebuilder:deprecatedversion:warning=will deprecated in 3.22 + Sentinel *RedisStatusInstanceSentinel `json:"sentinel,omitempty"` +} + +type RedisStatusInstanceRedis struct { + Size int32 `json:"size,omitempty"` + Ready int32 `json:"ready,omitempty"` +} + +type RedisStatusInstanceSentinel struct { + Size int32 `json:"size,omitempty"` + Ready int32 `json:"ready,omitempty"` + Service string `json:"service,omitempty"` + ClusterIP string `json:"clusterIp,omitempty"` + Port string `json:"port,omitempty"` +} + +type RedisStatusMaster struct { + Name string `json:"name"` + Status RedisStatusMasterStatus `json:"status"` + Address string `json:"address"` +} + +type RedisStatusMasterStatus string + +const ( + RedisStatusMasterOK RedisStatusMasterStatus = "ok" + RedisStatusMasterDown RedisStatusMasterStatus = "down" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.redis.replicas",description="Redis replicas" +// +kubebuilder:printcolumn:name="Sentinels",type="integer",JSONPath=".spec.sentinel.replicas",description="Redis sentinel replicas" +// +kubebuilder:printcolumn:name="Access",type="string",JSONPath=".spec.redis.expose.type",description="Instance access type" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Instance phase" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message",description="Instance status message" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since creation" + +// RedisFailover is the Schema for the redisfailovers API +type RedisFailover struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisFailoverSpec `json:"spec,omitempty"` + Status RedisFailoverStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// RedisFailoverList contains a list of RedisFailover +type RedisFailoverList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RedisFailover `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RedisFailover{}, &RedisFailoverList{}) +} diff --git a/api/databases/v1/redissentinel_types.go b/api/databases/v1/redissentinel_types.go new file mode 100644 index 0000000..4ba71b7 --- /dev/null +++ b/api/databases/v1/redissentinel_types.go @@ -0,0 +1,113 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/alauda/redis-operator/api/core" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RedisSentinelSpec defines the desired state of RedisSentinel +type RedisSentinelSpec struct { + // Image the redis sentinel image + Image string `json:"image,omitempty"` + // ImagePullPolicy the image pull policy + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + // ImagePullSecrets the image pull secrets + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // Replicas the number of sentinel replicas + // +kubebuilder:validation:Minimum=3 + Replicas int32 `json:"replicas,omitempty"` + // Resources the resources for sentinel + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + // Config the config for sentinel + CustomConfig map[string]string `json:"customConfig,omitempty"` + // Exporter defines the specification for the sentinel exporter + Exporter *SentinelExporter `json:"exporter,omitempty"` + // Expose + Expose core.InstanceAccess `json:"expose,omitempty"` + // PasswordSecret + PasswordSecret string `json:"passwordSecret,omitempty"` + // EnableTLS enable TLS for redis + EnableTLS bool `json:"enableTLS,omitempty"` + // ExternalTLSSecret the external TLS secret to use, if not provided, the operator will create one + ExternalTLSSecret string `json:"externalTLSSecret,omitempty"` + + Affinity *corev1.Affinity `json:"affinity,omitempty"` + SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + PodAnnotations map[string]string `json:"podAnnotations,omitempty"` + ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"` +} + +// SentinelPhase +type SentinelPhase string + +const ( + // SentinelCreating the sentinel creating phase + SentinelCreating SentinelPhase = "Creating" + // SentinelPaused the sentinel paused phase + SentinelPaused SentinelPhase = "Paused" + // SentinelReady the sentinel ready phase + SentinelReady SentinelPhase = "Ready" + // SentinelFail the sentinel fail phase + SentinelFail SentinelPhase = "Fail" +) + +// RedisSentinelStatus defines the observed state of RedisSentinel +type RedisSentinelStatus struct { + // Phase the status phase + Phase SentinelPhase `json:"phase,omitempty"` + // Message the status message + Message string `json:"message,omitempty"` + // Nodes the redis cluster nodes + Nodes []core.RedisNode `json:"nodes,omitempty"` + // TLSSecret the tls secret + TLSSecret string `json:"tlsSecret,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas",description="Sentinel replicas" +// +kubebuilder:printcolumn:name="Access",type="string",JSONPath=".spec.expose.type",description="Instance access type" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Instance phase" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message",description="Instance status message" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since creation" + +// RedisSentinel is the Schema for the redissentinels API +type RedisSentinel struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisSentinelSpec `json:"spec,omitempty"` + Status RedisSentinelStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RedisSentinelList contains a list of RedisSentinel +type RedisSentinelList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RedisSentinel `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RedisSentinel{}, &RedisSentinelList{}) +} diff --git a/api/databases.spotahome.com/v1/util.go b/api/databases/v1/util.go similarity index 73% rename from api/databases.spotahome.com/v1/util.go rename to api/databases/v1/util.go index 6bee944..c2aad74 100644 --- a/api/databases.spotahome.com/v1/util.go +++ b/api/databases/v1/util.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,23 +17,16 @@ limitations under the License. package v1 const ( - PhaseFail Phase = "Fail" - PhaseCreating Phase = "Creating" - PhasePending Phase = "Pending" - PhaseReady Phase = "Ready" - PhaseWaitingPodReady Phase = "WaitingPodReady" - PhasePaused Phase = "Paused" - DefaultRedisPort string = "6379" ) func (r *RedisFailoverStatus) SetPausedPhase(message string) { - r.Phase = PhasePaused + r.Phase = Paused r.Message = message } func (r *RedisFailoverStatus) SetFailedPhase(message string) { - r.Phase = PhaseFail + r.Phase = Fail r.Message = message } @@ -48,19 +41,19 @@ func (r *RedisFailoverStatus) SetMasterDown(ip string, port string) { } func (r *RedisFailoverStatus) SetWaitingPodReady(message string) { - r.Phase = PhaseWaitingPodReady + r.Phase = WaitingPodReady r.Message = message } func (r *RedisFailoverStatus) IsWaitingPodReady() bool { - return r.Phase == PhaseWaitingPodReady + return r.Phase == WaitingPodReady } func (r *RedisFailoverStatus) IsPaused() bool { - return r.Phase == PhasePaused + return r.Phase == Paused } func (r *RedisFailoverStatus) SetReady(message string) { - r.Phase = PhaseReady + r.Phase = Ready r.Message = message } diff --git a/api/databases/v1/validate.go b/api/databases/v1/validate.go new file mode 100644 index 0000000..8eb1a96 --- /dev/null +++ b/api/databases/v1/validate.go @@ -0,0 +1,55 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "fmt" + "os" + + "github.com/alauda/redis-operator/api/core/helper" +) + +const ( + maxNameLength = 48 + defaultRedisNumber = 2 +) + +// Validate set the values by default if not defined and checks if the values given are valid +// TODO: remove this validation and use the kubebuilder validation +func (r *RedisFailover) Validate() error { + if len(r.Name) > maxNameLength { + return fmt.Errorf("name length can't be higher than %d", maxNameLength) + } + + if r.Spec.Redis.Replicas <= 0 { + r.Spec.Redis.Replicas = defaultRedisNumber + } + if r.Spec.Redis.Expose.IPFamilyPrefer == "" { + r.Spec.Redis.Expose.IPFamilyPrefer = helper.GetDefaultIPFamily(os.Getenv("POD_IP")) + } + if r.Spec.Sentinel != nil { + if r.Spec.Sentinel.SentinelReference == nil { + if r.Spec.Sentinel.Replicas <= 0 { + r.Spec.Sentinel.Replicas = DefaultSentinelNumber + } + if r.Spec.Sentinel.Expose.IPFamilyPrefer == "" { + r.Spec.Sentinel.Expose.IPFamilyPrefer = r.Spec.Redis.Expose.IPFamilyPrefer + } + } + } + return nil +} diff --git a/api/databases.spotahome.com/v1/zz_generated.deepcopy.go b/api/databases/v1/zz_generated.deepcopy.go similarity index 72% rename from api/databases.spotahome.com/v1/zz_generated.deepcopy.go rename to api/databases/v1/zz_generated.deepcopy.go index e28b234..cb2fc90 100644 --- a/api/databases.spotahome.com/v1/zz_generated.deepcopy.go +++ b/api/databases/v1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2023 The RedisOperator Authors. @@ -8,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -22,7 +21,7 @@ limitations under the License. package v1 import ( - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "github.com/alauda/redis-operator/api/core" corev1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -43,55 +42,36 @@ func (in *AuthSettings) DeepCopy() *AuthSettings { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackup) DeepCopyInto(out *RedisBackup) { +func (in *Authorization) DeepCopyInto(out *Authorization) { *out = *in - if in.Schedule != nil { - in, out := &in.Schedule, &out.Schedule - *out = make([]Schedule, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackup. -func (in *RedisBackup) DeepCopy() *RedisBackup { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authorization. +func (in *Authorization) DeepCopy() *Authorization { if in == nil { return nil } - out := new(RedisBackup) + out := new(Authorization) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupStorage) DeepCopyInto(out *RedisBackupStorage) { +func (in *MonitorStatus) DeepCopyInto(out *MonitorStatus) { *out = *in - out.Size = in.Size.DeepCopy() -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupStorage. -func (in *RedisBackupStorage) DeepCopy() *RedisBackupStorage { - if in == nil { - return nil + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]SentinelMonitorNode, len(*in)) + copy(*out, *in) } - out := new(RedisBackupStorage) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupTarget) DeepCopyInto(out *RedisBackupTarget) { - *out = *in - out.S3Option = in.S3Option } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupTarget. -func (in *RedisBackupTarget) DeepCopy() *RedisBackupTarget { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonitorStatus. +func (in *MonitorStatus) DeepCopy() *MonitorStatus { if in == nil { return nil } - out := new(RedisBackupTarget) + out := new(MonitorStatus) in.DeepCopyInto(out) return out } @@ -99,6 +79,7 @@ func (in *RedisBackupTarget) DeepCopy() *RedisBackupTarget { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisExporter) DeepCopyInto(out *RedisExporter) { *out = *in + in.Resources.DeepCopyInto(&out.Resources) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisExporter. @@ -111,28 +92,6 @@ func (in *RedisExporter) DeepCopy() *RedisExporter { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisExpose) DeepCopyInto(out *RedisExpose) { - *out = *in - if in.DataStorageNodePortMap != nil { - in, out := &in.DataStorageNodePortMap, &out.DataStorageNodePortMap - *out = make(map[string]int32, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisExpose. -func (in *RedisExpose) DeepCopy() *RedisExpose { - if in == nil { - return nil - } - out := new(RedisExpose) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisFailover) DeepCopyInto(out *RedisFailover) { *out = *in @@ -160,6 +119,28 @@ func (in *RedisFailover) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisFailoverDetailedStatus) DeepCopyInto(out *RedisFailoverDetailedStatus) { + *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]core.RedisDetailedNode, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisFailoverDetailedStatus. +func (in *RedisFailoverDetailedStatus) DeepCopy() *RedisFailoverDetailedStatus { + if in == nil { + return nil + } + out := new(RedisFailoverDetailedStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisFailoverList) DeepCopyInto(out *RedisFailoverList) { *out = *in @@ -196,15 +177,22 @@ func (in *RedisFailoverList) DeepCopyObject() runtime.Object { func (in *RedisFailoverSpec) DeepCopyInto(out *RedisFailoverSpec) { *out = *in in.Redis.DeepCopyInto(&out.Redis) - in.Sentinel.DeepCopyInto(&out.Sentinel) + if in.Sentinel != nil { + in, out := &in.Sentinel, &out.Sentinel + *out = new(SentinelSettings) + (*in).DeepCopyInto(*out) + } out.Auth = in.Auth if in.LabelWhitelist != nil { in, out := &in.LabelWhitelist, &out.LabelWhitelist *out = make([]string, len(*in)) copy(*out, *in) } - in.ServiceMonitor.DeepCopyInto(&out.ServiceMonitor) - in.Expose.DeepCopyInto(&out.Expose) + if in.ServiceID != nil { + in, out := &in.ServiceID, &out.ServiceID + *out = new(int32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisFailoverSpec. @@ -220,10 +208,21 @@ func (in *RedisFailoverSpec) DeepCopy() *RedisFailoverSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisFailoverStatus) DeepCopyInto(out *RedisFailoverStatus) { *out = *in - in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) - in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) - out.Instance = in.Instance + in.Instance.DeepCopyInto(&out.Instance) out.Master = in.Master + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]core.RedisNode, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Monitor.DeepCopyInto(&out.Monitor) + if in.DetailedStatusRef != nil { + in, out := &in.DetailedStatusRef, &out.DetailedStatusRef + *out = new(corev1.ObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisFailoverStatus. @@ -237,49 +236,72 @@ func (in *RedisFailoverStatus) DeepCopy() *RedisFailoverStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisRestore) DeepCopyInto(out *RedisRestore) { +func (in *RedisSentinel) DeepCopyInto(out *RedisSentinel) { *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisRestore. -func (in *RedisRestore) DeepCopy() *RedisRestore { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinel. +func (in *RedisSentinel) DeepCopy() *RedisSentinel { if in == nil { return nil } - out := new(RedisRestore) + out := new(RedisSentinel) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisSentinel) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisServiceMonitorSpec) DeepCopyInto(out *RedisServiceMonitorSpec) { +func (in *RedisSentinelList) DeepCopyInto(out *RedisSentinelList) { *out = *in - if in.MetricRelabelConfigs != nil { - in, out := &in.MetricRelabelConfigs, &out.MetricRelabelConfigs - *out = make([]*monitoringv1.RelabelConfig, len(*in)) + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RedisSentinel, len(*in)) for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(monitoringv1.RelabelConfig) - (*in).DeepCopyInto(*out) - } + (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisServiceMonitorSpec. -func (in *RedisServiceMonitorSpec) DeepCopy() *RedisServiceMonitorSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelList. +func (in *RedisSentinelList) DeepCopy() *RedisSentinelList { if in == nil { return nil } - out := new(RedisServiceMonitorSpec) + out := new(RedisSentinelList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisSentinelList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisSettings) DeepCopyInto(out *RedisSettings) { +func (in *RedisSentinelSpec) DeepCopyInto(out *RedisSentinelSpec) { *out = *in + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } in.Resources.DeepCopyInto(&out.Resources) if in.CustomConfig != nil { in, out := &in.CustomConfig, &out.CustomConfig @@ -288,8 +310,12 @@ func (in *RedisSettings) DeepCopyInto(out *RedisSettings) { (*out)[key] = val } } - in.Storage.DeepCopyInto(&out.Storage) - out.Exporter = in.Exporter + if in.Exporter != nil { + in, out := &in.Exporter, &out.Exporter + *out = new(SentinelExporter) + **out = **in + } + in.Expose.DeepCopyInto(&out.Expose) if in.Affinity != nil { in, out := &in.Affinity, &out.Affinity *out = new(corev1.Affinity) @@ -300,11 +326,97 @@ func (in *RedisSettings) DeepCopyInto(out *RedisSettings) { *out = new(corev1.PodSecurityContext) (*in).DeepCopyInto(*out) } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ServiceAnnotations != nil { + in, out := &in.ServiceAnnotations, &out.ServiceAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelSpec. +func (in *RedisSentinelSpec) DeepCopy() *RedisSentinelSpec { + if in == nil { + return nil + } + out := new(RedisSentinelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelStatus) DeepCopyInto(out *RedisSentinelStatus) { + *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]core.RedisNode, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelStatus. +func (in *RedisSentinelStatus) DeepCopy() *RedisSentinelStatus { + if in == nil { + return nil + } + out := new(RedisSentinelStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSettings) DeepCopyInto(out *RedisSettings) { + *out = *in if in.ImagePullSecrets != nil { in, out := &in.ImagePullSecrets, &out.ImagePullSecrets *out = make([]corev1.LocalObjectReference, len(*in)) copy(*out, *in) } + in.Resources.DeepCopyInto(&out.Resources) + if in.CustomConfig != nil { + in, out := &in.CustomConfig, &out.CustomConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Storage.DeepCopyInto(&out.Storage) + in.Exporter.DeepCopyInto(&out.Exporter) + in.Expose.DeepCopyInto(&out.Expose) + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(corev1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations *out = make([]corev1.Toleration, len(*in)) @@ -351,7 +463,11 @@ func (in *RedisSettings) DeepCopy() *RedisSettings { func (in *RedisStatusInstance) DeepCopyInto(out *RedisStatusInstance) { *out = *in out.Redis = in.Redis - out.Sentinel = in.Sentinel + if in.Sentinel != nil { + in, out := &in.Sentinel, &out.Sentinel + *out = new(RedisStatusInstanceSentinel) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisStatusInstance. @@ -412,6 +528,11 @@ func (in *RedisStatusMaster) DeepCopy() *RedisStatusMaster { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisStorage) DeepCopyInto(out *RedisStorage) { *out = *in + if in.EmptyDir != nil { + in, out := &in.EmptyDir, &out.EmptyDir + *out = new(corev1.EmptyDirVolumeSource) + (*in).DeepCopyInto(*out) + } if in.PersistentVolumeClaim != nil { in, out := &in.PersistentVolumeClaim, &out.PersistentVolumeClaim *out = new(corev1.PersistentVolumeClaim) @@ -430,48 +551,52 @@ func (in *RedisStorage) DeepCopy() *RedisStorage { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *S3Option) DeepCopyInto(out *S3Option) { +func (in *SentinelExporter) DeepCopyInto(out *SentinelExporter) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Option. -func (in *S3Option) DeepCopy() *S3Option { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SentinelExporter. +func (in *SentinelExporter) DeepCopy() *SentinelExporter { if in == nil { return nil } - out := new(S3Option) + out := new(SentinelExporter) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Schedule) DeepCopyInto(out *Schedule) { +func (in *SentinelMonitorNode) DeepCopyInto(out *SentinelMonitorNode) { *out = *in - in.Storage.DeepCopyInto(&out.Storage) - out.Target = in.Target } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Schedule. -func (in *Schedule) DeepCopy() *Schedule { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SentinelMonitorNode. +func (in *SentinelMonitorNode) DeepCopy() *SentinelMonitorNode { if in == nil { return nil } - out := new(Schedule) + out := new(SentinelMonitorNode) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SentinelExporter) DeepCopyInto(out *SentinelExporter) { +func (in *SentinelReference) DeepCopyInto(out *SentinelReference) { *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]SentinelMonitorNode, len(*in)) + copy(*out, *in) + } + out.Auth = in.Auth } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SentinelExporter. -func (in *SentinelExporter) DeepCopy() *SentinelExporter { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SentinelReference. +func (in *SentinelReference) DeepCopy() *SentinelReference { if in == nil { return nil } - out := new(SentinelExporter) + out := new(SentinelReference) in.DeepCopyInto(out) return out } @@ -479,63 +604,24 @@ func (in *SentinelExporter) DeepCopy() *SentinelExporter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SentinelSettings) DeepCopyInto(out *SentinelSettings) { *out = *in - in.Resources.DeepCopyInto(&out.Resources) - if in.CustomConfig != nil { - in, out := &in.CustomConfig, &out.CustomConfig - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Command != nil { - in, out := &in.Command, &out.Command - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Affinity != nil { - in, out := &in.Affinity, &out.Affinity - *out = new(corev1.Affinity) - (*in).DeepCopyInto(*out) - } - if in.SecurityContext != nil { - in, out := &in.SecurityContext, &out.SecurityContext - *out = new(corev1.PodSecurityContext) + in.RedisSentinelSpec.DeepCopyInto(&out.RedisSentinelSpec) + if in.SentinelReference != nil { + in, out := &in.SentinelReference, &out.SentinelReference + *out = new(SentinelReference) (*in).DeepCopyInto(*out) } - if in.ImagePullSecrets != nil { - in, out := &in.ImagePullSecrets, &out.ImagePullSecrets - *out = make([]corev1.LocalObjectReference, len(*in)) - copy(*out, *in) - } - if in.Tolerations != nil { - in, out := &in.Tolerations, &out.Tolerations - *out = make([]corev1.Toleration, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.NodeSelector != nil { - in, out := &in.NodeSelector, &out.NodeSelector + if in.MonitorConfig != nil { + in, out := &in.MonitorConfig, &out.MonitorConfig *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } - if in.PodAnnotations != nil { - in, out := &in.PodAnnotations, &out.PodAnnotations - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.ServiceAnnotations != nil { - in, out := &in.ServiceAnnotations, &out.ServiceAnnotations - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } + if in.Quorum != nil { + in, out := &in.Quorum, &out.Quorum + *out = new(int32) + **out = **in } - out.Exporter = in.Exporter } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SentinelSettings. diff --git a/api/redis/v1/groupversion_info.go b/api/middleware/redis/v1/groupversion_info.go similarity index 96% rename from api/redis/v1/groupversion_info.go rename to api/middleware/redis/v1/groupversion_info.go index 66203bb..e78167d 100644 --- a/api/redis/v1/groupversion_info.go +++ b/api/middleware/redis/v1/groupversion_info.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/api/redis/v1/redisuser_types.go b/api/middleware/redis/v1/redisuser_types.go similarity index 69% rename from api/redis/v1/redisuser_types.go rename to api/middleware/redis/v1/redisuser_types.go index d4ac5c6..5a005d0 100644 --- a/api/redis/v1/redisuser_types.go +++ b/api/middleware/redis/v1/redisuser_types.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,44 +17,38 @@ limitations under the License. package v1 import ( - "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/api/core" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // RedisUserSpec defines the desired state of RedisUser type RedisUserSpec struct { - // Username - // +kubebuilder:validation:MaxLength=64 - Username string `json:"username"` + // Redis Username (required) + Username string `json:"username"` //用户名 // Redis Password secret name, key is password - PasswordSecrets []string `json:"passwordSecrets,omitempty"` - // AclRules acl rules - // +kubebuilder:validation:MaxLength=4096 - AclRules string `json:"aclRules,omitempty"` + PasswordSecrets []string `json:"passwordSecrets,omitempty"` //密码secret + // redis acl rules string + AclRules string `json:"aclRules,omitempty"` //acl规则 // Redis instance Name (required) // +kubebuilder:validation:MaxLength=63 // +kubebuilder:validation:MinLength=1 - RedisName string `json:"redisName"` + RedisName string `json:"redisName"` //redisname // redis user account type // +kubebuilder:validation:Enum=system;custom;default - AccountType AccountType `json:"accountType,omitempty"` + AccountType AccountType `json:"accountType,omitempty"` //账户类型 system,custom,default // redis user account type - // +kubebuilder:validation:Enum=sentinel;cluster - // +kubebuilder:default=sentinel - Arch redis.RedisArch `json:"arch,omitempty"` + // +kubebuilder:validation:Enum=sentinel;cluster;standalone + Arch core.Arch `json:"arch,omitempty"` //架构类型 } -// AccountType type AccountType string const ( - // System only operator is the system account System AccountType = "system" Custom AccountType = "custom" Default AccountType = "default" ) -// Phase type Phase string const ( @@ -65,14 +59,11 @@ const ( // RedisUserStatus defines the observed state of RedisUser type RedisUserStatus struct { - // LastUpdatedSuccess is the last time the user was successfully updated. - LastUpdatedSuccess string `json:"lastUpdateSuccess,omitempty"` + LastUpdatedSuccess string `json:"lastUpdateSuccess,omitempty"` //最后更新时间 // +kubebuilder:validation:Enum=Fail;Success;Pending - Phase Phase `json:"Phase,omitempty"` - // Message - Message string `json:"message,omitempty"` - // AclRules last configed acl rule - AclRules string `json:"aclRules,omitempty"` + Phase Phase `json:"Phase,omitempty"` //Fail or Success + Message string `json:"message,omitempty"` //失败信息 + AclRules string `json:"aclRules,omitempty"` //redis中的规则,会去除密码 } //+kubebuilder:object:root=true diff --git a/api/middleware/redis/v1/redisuser_webhook.go b/api/middleware/redis/v1/redisuser_webhook.go new file mode 100644 index 0000000..e45a018 --- /dev/null +++ b/api/middleware/redis/v1/redisuser_webhook.go @@ -0,0 +1,293 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v1 + +import ( + "context" + "fmt" + "slices" + "strings" + + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" + security "github.com/alauda/redis-operator/pkg/security/password" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/pkg/types/user" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +const ( + RedisUserFinalizer = "redisusers.redis.middleware.alauda.io/finalizer" + + ACLSupportedVersionAnnotationKey = "middleware.alauda.io/acl-supported-version" +) + +// log is for logging in this package. +var logger = logf.Log.WithName("RedisUser") +var dyClient client.Client + +func (r *RedisUser) SetupWebhookWithManager(mgr ctrl.Manager) error { + dyClient = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:verbs=create;update,path=/mutate-redis-middleware-alauda-io-v1-redisuser,mutating=true,failurePolicy=fail,groups=redis.middleware.alauda.io,resources=redisusers,versions=v1,name=mredisuser.kb.io,sideEffects=none,admissionReviewVersions=v1 +//+kubebuilder:webhook:verbs=create;update;delete,path=/validate-redis-middleware-alauda-io-v1-redisuser,mutating=false,failurePolicy=fail,groups=redis.middleware.alauda.io,resources=redisusers,versions=v1,name=vredisuser.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var _ webhook.Validator = &RedisUser{} +var _ webhook.Defaulter = &RedisUser{} + +func (r *RedisUser) Default() { + logger.V(3).Info("default", "redisUser.name", r.Name) + if r.Labels == nil { + r.Labels = make(map[string]string) + } + r.Labels[builder.ManagedByLabel] = "redis-operator" + r.Labels[builder.InstanceNameLabel] = r.Spec.RedisName + + if r.GetDeletionTimestamp() != nil { + return + } + rule, err := user.NewRule(r.Spec.AclRules) + if err != nil { + return + } + + version := redis.RedisVersion(config.GetRedisVersion(config.GetDefaultRedisImage())) + switch r.Spec.Arch { + case core.RedisSentinel, core.RedisStandalone: + rf := &databasesv1.RedisFailover{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName}, rf); err != nil { + logger.Error(err, "get redis failover failed", "name", r.Name) + } else { + r.OwnerReferences = util.BuildOwnerReferencesWithParents(rf) + version = redis.RedisVersion(config.GetRedisVersion(rf.Spec.Redis.Image)) + } + case core.RedisCluster: + cluster := &clusterv1.DistributedRedisCluster{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName}, cluster); err != nil { + logger.Error(err, "get redis cluster failed", "name", r.Name) + } else { + r.OwnerReferences = util.BuildOwnerReferencesWithParents(cluster) + version = redis.RedisVersion(config.GetRedisVersion(cluster.Spec.Image)) + } + } + + if r.Spec.AccountType != System && rule.IsCommandEnabled("acl", []string{"all", "admin", "slow", "dangerous"}) { + if slices.Contains(rule.AllowedCommands, "acl") { + cmds := rule.AllowedCommands[:0] + for _, cmd := range rule.AllowedCommands { + if cmd != "acl" { + cmds = append(cmds, cmd) + } + } + rule.AllowedCommands = cmds + } + rule.DisallowedCommands = append(rule.DisallowedCommands, "acl") + } + if version.IsACL2Supported() { + rule = user.PatchRedisPubsubRules(rule) + } + r.Spec.AclRules = rule.Encode() +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *RedisUser) ValidateCreate() (admission.Warnings, error) { + if r.Spec.AccountType == System { + if r.Spec.Username != "operator" { + return nil, fmt.Errorf("system account username must be operator") + } + return nil, nil + } + if r.Spec.AccountType == Default && r.Spec.Username != "default" { + return nil, fmt.Errorf("default account username must be default") + } + if len(r.Spec.PasswordSecrets) == 0 && r.Spec.AccountType != Default { + return nil, fmt.Errorf("password secret can not be empty") + } + + rule, err := user.NewRule(strings.ToLower(r.Spec.AclRules)) + if err != nil { + return nil, err + } + if err := rule.Validate(true); err != nil { + return nil, err + } + for _, v := range r.Spec.PasswordSecrets { + if v == "" { + return nil, fmt.Errorf("password secret can not be empty") + } + secret := &v1.Secret{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: v, + }, secret); err != nil { + return nil, err + } else if err := security.PasswordValidate(string(secret.Data["password"]), 8, 32); err != nil { + return nil, err + } + } + + if r.Spec.AccountType == Custom { + switch r.Spec.Arch { + case core.RedisSentinel, core.RedisStandalone: + rf := &databasesv1.RedisFailover{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName}, rf); err != nil { + return nil, err + } + if rf.Status.Phase != databasesv1.Ready { + return nil, fmt.Errorf("redis failover %s is not ready", r.Spec.RedisName) + } + case core.RedisCluster: + cluster := clusterv1.DistributedRedisCluster{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName}, &cluster); err != nil { + return nil, err + } + if cluster.Status.Status != clusterv1.ClusterStatusOK { + return nil, fmt.Errorf("redis cluster %s is not ready", r.Spec.RedisName) + } + } + } + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *RedisUser) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { + if r.Spec.AccountType == System { + if r.Spec.Username != "operator" { + return nil, fmt.Errorf("system account username must be operator") + } + return nil, nil + } + if r.Spec.AccountType == Default && r.Spec.Username != "default" { + return nil, fmt.Errorf("default account username must be default") + } + if r.GetDeletionTimestamp() != nil { + return nil, nil + } + if len(r.Spec.PasswordSecrets) == 0 && r.Spec.AccountType != Default { + return nil, fmt.Errorf("password secret can not be empty") + } + + //??? + // if !controllerutil.ContainsFinalizer(r, RedisUserFinalizer) { + // return nil + // } + + rule, err := user.NewRule(strings.ToLower(r.Spec.AclRules)) + if err != nil { + return nil, err + } + if err := rule.Validate(true); err != nil { + return nil, err + } + for _, v := range r.Spec.PasswordSecrets { + if v == "" { + continue + } + secret := &v1.Secret{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: v, + }, secret); err != nil { + return nil, err + } else if err := security.PasswordValidate(string(secret.Data["password"]), 8, 32); err != nil { + return nil, err + } + } + + if r.Spec.AccountType == Custom { + switch r.Spec.Arch { + case core.RedisSentinel, core.RedisStandalone: + rf := &databasesv1.RedisFailover{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName}, rf); err != nil { + return nil, err + } + if rf.Status.Phase != databasesv1.Ready { + return nil, fmt.Errorf("redis failover %s is not ready", r.Spec.RedisName) + } + case core.RedisCluster: + cluster := clusterv1.DistributedRedisCluster{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName}, &cluster); err != nil { + return nil, err + } + if cluster.Status.Status != clusterv1.ClusterStatusOK { + return nil, fmt.Errorf("redis cluster %s is not ready", r.Spec.RedisName) + } + } + } + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *RedisUser) ValidateDelete() (admission.Warnings, error) { + if r.Spec.Username == user.DefaultUserName || r.Spec.Username == user.DefaultOperatorUserName { + switch r.Spec.Arch { + case core.RedisSentinel, core.RedisStandalone: + rf := databasesv1.RedisFailover{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName, + }, &rf); errors.IsNotFound(err) { + return nil, nil + } else if err != nil { + return nil, err + } else if rf.GetDeletionTimestamp() != nil { + return nil, nil + } + case core.RedisCluster: + cluster := clusterv1.DistributedRedisCluster{} + if err := dyClient.Get(context.Background(), types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.RedisName, + }, &cluster); errors.IsNotFound(err) { + return nil, nil + } else if err != nil { + return nil, err + } else if cluster.GetDeletionTimestamp() != nil { + return nil, nil + } + } + return nil, fmt.Errorf("user %s can not be deleted", r.GetName()) + } + return nil, nil +} diff --git a/api/middleware/redis/v1/zz_generated.deepcopy.go b/api/middleware/redis/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000..5ac6b7e --- /dev/null +++ b/api/middleware/redis/v1/zz_generated.deepcopy.go @@ -0,0 +1,119 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisUser) DeepCopyInto(out *RedisUser) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUser. +func (in *RedisUser) DeepCopy() *RedisUser { + if in == nil { + return nil + } + out := new(RedisUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisUser) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisUserList) DeepCopyInto(out *RedisUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RedisUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUserList. +func (in *RedisUserList) DeepCopy() *RedisUserList { + if in == nil { + return nil + } + out := new(RedisUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisUserList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisUserSpec) DeepCopyInto(out *RedisUserSpec) { + *out = *in + if in.PasswordSecrets != nil { + in, out := &in.PasswordSecrets, &out.PasswordSecrets + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUserSpec. +func (in *RedisUserSpec) DeepCopy() *RedisUserSpec { + if in == nil { + return nil + } + out := new(RedisUserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisUserStatus) DeepCopyInto(out *RedisUserStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUserStatus. +func (in *RedisUserStatus) DeepCopy() *RedisUserStatus { + if in == nil { + return nil + } + out := new(RedisUserStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/middleware/v1/grouversion_info.go b/api/middleware/v1/grouversion_info.go new file mode 100644 index 0000000..589fc01 --- /dev/null +++ b/api/middleware/v1/grouversion_info.go @@ -0,0 +1,35 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +kubebuilder:object:generate=true +// +groupName=middleware.alauda.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "middleware.alauda.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/middleware/v1/helper/helper.go b/api/middleware/v1/helper/helper.go new file mode 100644 index 0000000..79fccde --- /dev/null +++ b/api/middleware/v1/helper/helper.go @@ -0,0 +1,116 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helper + +import ( + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/alauda/redis-operator/api/core" + "github.com/samber/lo" +) + +var ( + shardIndexReg = regexp.MustCompile(`^.*-(\d+)$`) +) + +func parseShardIndex(name string) int { + if shardIndexReg.MatchString(name) { + matches := shardIndexReg.FindStringSubmatch(name) + if len(matches) == 2 { + val, _ := strconv.ParseInt(matches[1], 10, 32) + return int(val) + } + } + return 0 +} + +// ExtractShardDatasetUsedMemory extract the used memory dataset from the shard ordered by index +func ExtractShardDatasetUsedMemory(name string, shards int, nodes []core.RedisDetailedNode) (ret []int64) { + if shards == 0 { + return + } + data := map[int]int64{} + for _, node := range nodes { + name := strings.ReplaceAll(node.StatefulSet, "-"+name, "") + index := parseShardIndex(name) + if val, ok := data[index]; ok { + data[index] = lo.Max([]int64{val, node.UsedMemoryDataset}) + } else { + data[index] = node.UsedMemoryDataset + } + } + + ret = make([]int64, lo.Max([]int{len(data), shards})) + for index, val := range data { + ret[index] = val + } + return +} + +func BuildRenameCommand(rawRename string) string { + // check restart config + renameConfigs := map[string]string{ + "flushall": "", + "flushdb": "", + } + if rawRename != "" { + fields := strings.Fields(strings.ToLower(rawRename)) + if len(fields)%2 == 0 { + for i := 0; i < len(fields); i += 2 { + renameConfigs[fields[i]] = fields[i+1] + } + } + } + keys := []string{} + for key := range renameConfigs { + keys = append(keys, key) + } + sort.Strings(keys) + + renameVal := "" + for _, key := range keys { + val := renameConfigs[key] + if val == "" { + val = `""` + } + renameVal = strings.TrimSpace(fmt.Sprintf("%s %s %s", renameVal, key, val)) + } + return renameVal +} + +func CalculateNodeCount(arch core.Arch, masterCount *int32, replicaCount *int32) int { + switch arch { + case core.RedisCluster: + if replicaCount != nil { + return int(*masterCount) * int((*replicaCount)+1) + } + return int(*masterCount) + case core.RedisSentinel: + if replicaCount != nil { + return int(*masterCount) + int(*replicaCount) + } + return int(*masterCount) + case core.RedisStandalone: + return 1 + default: + return 0 + } +} diff --git a/api/middleware/v1/helper/helper_test.go b/api/middleware/v1/helper/helper_test.go new file mode 100644 index 0000000..589834d --- /dev/null +++ b/api/middleware/v1/helper/helper_test.go @@ -0,0 +1,278 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helper + +import ( + "reflect" + "testing" + + "github.com/alauda/redis-operator/api/core" + "k8s.io/utils/pointer" +) + +func Test_parseShardIndex(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want int + }{ + { + name: "drc-test-0-0", + args: args{ + name: "drc-test-0-0", + }, + want: 0, + }, + { + name: "drc-test-0-999", + args: args{ + name: "drc-test-0-999", + }, + want: 999, + }, + { + name: "rfr-test", + args: args{ + name: "rfr-test", + }, + want: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseShardIndex(tt.args.name); got != tt.want { + t.Errorf("parseShardIndex() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestExtractShardDatasetUsedMemory(t *testing.T) { + type args struct { + name string + shards int + nodes []core.RedisDetailedNode + } + tests := []struct { + name string + args args + wantRet []int64 + }{ + { + name: "redis cluster name=test", + args: args{ + name: "test", + shards: 3, + nodes: []core.RedisDetailedNode{ + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-1"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-1"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-2"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-2"}, UsedMemoryDataset: 110}, + }, + }, + wantRet: []int64{100, 100, 110}, + }, + { + name: "redis cluster name=test-0", + args: args{ + name: "test-0", + shards: 3, + nodes: []core.RedisDetailedNode{ + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-0"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-0"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-1"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-1"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-2"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-2"}, UsedMemoryDataset: 110}, + }, + }, + wantRet: []int64{100, 100, 110}, + }, + { + name: "redis cluster with datasize different", + args: args{ + name: "test-0", + shards: 3, + nodes: []core.RedisDetailedNode{ + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-0"}, UsedMemoryDataset: 10}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-0"}, UsedMemoryDataset: 110}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-1"}, UsedMemoryDataset: 0}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-1"}, UsedMemoryDataset: 0}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-2"}, UsedMemoryDataset: 0}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-2"}, UsedMemoryDataset: 130}, + }, + }, + wantRet: []int64{110, 0, 130}, + }, + { + name: "redis cluster with not enough nodes", + args: args{ + name: "test-0", + shards: 6, + nodes: []core.RedisDetailedNode{ + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-0"}, UsedMemoryDataset: 10}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-0"}, UsedMemoryDataset: 110}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-1"}, UsedMemoryDataset: 0}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-1"}, UsedMemoryDataset: 0}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-2"}, UsedMemoryDataset: 0}, + {RedisNode: core.RedisNode{StatefulSet: "drc-test-0-2"}, UsedMemoryDataset: 130}, + }, + }, + wantRet: []int64{110, 0, 130, 0, 0, 0}, + }, + { + name: "redis sentinel name=test", + args: args{ + name: "test", + shards: 1, + nodes: []core.RedisDetailedNode{ + {RedisNode: core.RedisNode{StatefulSet: "rfr-test"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "rfr-test"}, UsedMemoryDataset: 110}, + }, + }, + wantRet: []int64{110}, + }, + { + name: "redis sentinel name=test-0", + args: args{ + name: "test-0", + shards: 1, + nodes: []core.RedisDetailedNode{ + {RedisNode: core.RedisNode{StatefulSet: "rfr-test-0"}, UsedMemoryDataset: 100}, + {RedisNode: core.RedisNode{StatefulSet: "rfr-test-0"}, UsedMemoryDataset: 100}, + }, + }, + wantRet: []int64{100}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotRet := ExtractShardDatasetUsedMemory(tt.args.name, tt.args.shards, tt.args.nodes); !reflect.DeepEqual(gotRet, tt.wantRet) { + t.Errorf("ExtractShardDatasetUsedMemory() = %v, want %v", gotRet, tt.wantRet) + } + }) + } +} + +func TestBuildRenameCommand(t *testing.T) { + type args struct { + rawRename string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + rawRename: "", + }, + want: `flushall "" flushdb ""`, + }, + { + name: "with keys disabled", + args: args{ + rawRename: `keys ""`, + }, + want: `flushall "" flushdb "" keys ""`, + }, + { + name: "with keys renamed", + args: args{ + rawRename: `keys "abc123"`, + }, + want: `flushall "" flushdb "" keys "abc123"`, + }, + { + name: "with open flushall", + args: args{ + rawRename: "flushall flushall", + }, + want: `flushall flushall flushdb ""`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := BuildRenameCommand(tt.args.rawRename); got != tt.want { + t.Errorf("BuildRenameCommand() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalculateNodeCount(t *testing.T) { + type args struct { + arch core.Arch + masterCount *int32 + replicaCount *int32 + } + tests := []struct { + name string + args args + want int + }{ + { + name: "redis cluster with replicas", + args: args{ + arch: core.RedisCluster, + masterCount: pointer.Int32(3), + replicaCount: pointer.Int32(1), + }, + want: 6, + }, + { + name: "redis cluster without replicas", + args: args{ + arch: core.RedisCluster, + masterCount: pointer.Int32(3), + replicaCount: nil, + }, + want: 3, + }, + { + name: "redis sentinel/standalone with replicas", + args: args{ + arch: core.RedisSentinel, + masterCount: pointer.Int32(1), + replicaCount: pointer.Int32(2), + }, + want: 3, + }, + { + name: "redis sentinel/standalone without replicas", + args: args{ + arch: core.RedisSentinel, + masterCount: pointer.Int32(1), + replicaCount: nil, + }, + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CalculateNodeCount(tt.args.arch, tt.args.masterCount, tt.args.replicaCount); got != tt.want { + t.Errorf("CalculateNodeCount() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/api/middleware/v1/redis_types.go b/api/middleware/v1/redis_types.go new file mode 100644 index 0000000..341224e --- /dev/null +++ b/api/middleware/v1/redis_types.go @@ -0,0 +1,405 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + redisfailoverv1 "github.com/alauda/redis-operator/api/databases/v1" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// UpgradeOption defines the upgrade strategy for the Redis instance. +type UpgradeOption struct { + // CRVersion indicates the version to upgrade to. + CRVersion string `json:"crVersion,omitempty"` + // AutoUpgrade whether upgrade automatically + AutoUpgrade *bool `json:"autoUpgrade,omitempty"` +} + +// UpgradeStatus indicates the status of the bundle upgrade. +type UpgradeStatus struct { + // CRVersion indicates the version to upgrade to. + CRVersion string `json:"crVersion,omitempty"` + // Message indicates the message of the upgrade. + Message string `json:"message,omitempty"` +} + +// SentinelSettings defines the specification of the sentinel cluster +type SentinelSettings struct { + redisfailoverv1.RedisSentinelSpec `json:",inline"` + // ExternalSentinel defines the sentinel reference + ExternalSentinel *redisfailoverv1.SentinelReference `json:"external,omitempty"` +} + +// InstanceAccess +type InstanceAccess struct { + core.InstanceAccessBase `json:",inline"` + + // EnableNodePort defines if the nodeport is enabled + // TODO: remove this field in 3.22 + // +kubebuilder:deprecated:warning="use serviceType instead" + EnableNodePort bool `json:"enableNodePort,omitempty"` +} + +// RedisSpec defines the desired state of Redis +type RedisSpec struct { + // Version supports 5.0, 6.0, 6.2, 7.0, 7.2, 7.4 + // +kubebuilder:validation:Enum="5.0";"6.0";"6.2";"7.0";"7.2";"7.4" + Version string `json:"version"` + // Arch supports cluster, sentinel + // +kubebuilder:validation:Enum="cluster";"sentinel";"standalone" + Arch core.Arch `json:"arch"` + // Resources for setting resource requirements for the Pod Resources *v1.ResourceRequirements + Resources *corev1.ResourceRequirements `json:"resources"` + + // Persistent for Redis + Persistent *RedisPersistent `json:"persistent,omitempty"` + // PersistentSize set the size of the persistent volume for the Redis + PersistentSize *resource.Quantity `json:"persistentSize,omitempty"` + // PasswordSecret set the Kubernetes Secret containing the Redis password PasswordSecret string,key `password` + PasswordSecret string `json:"passwordSecret,omitempty"` + // Replicas defines desired number of replicas for Redis + Replicas *RedisReplicas `json:"replicas,omitempty"` + // Affinity specifies the affinity for the Pod + // +optional + Affinity *corev1.Affinity `json:"affinity,omitempty"` + // AffinityPolicy support SoftAntiAffinity, AntiAffinityInSharding, AntiAffinity, Default SoftAntiAffinity + // +optional + // +kubebuilder:validation:Enum="SoftAntiAffinity";"AntiAffinityInSharding";"AntiAffinity" + AffinityPolicy core.AffinityPolicy `json:"affinityPolicy,omitempty"` + // NodeSelector specifies the node selector for the Pod + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // tolerations defines tolerations for the Pod + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + // SecurityContext sets security attributes for the Pod SecurityContex + // +optional + SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` + // CustomConfig defines custom Redis configuration settings. Some of these settings can be modified using the config set command at runtime. + // +optional + CustomConfig map[string]string `json:"customConfig,omitempty"` + // SentinelCustomConfig defines custom Sentinel configuration settings + // +kubebuilder:deprecatedversion:warning=remove in 3.20 use sentinel.customConfig instead + SentinelCustomConfig map[string]string `json:"sentinelCustomConfig,omitempty"` + // RedisProxy defines RedisProxy settings + // +optional + // +kubebuilder:deprecatedversion:warning=remove in 3.20 + RedisProxy *RedisProxy `json:"redisProxy,omitempty"` + // PodAnnotations holds Kubernetes Pod annotations PodAnnotations + // +optional + PodAnnotations map[string]string `json:"podAnnotations,omitempty"` + // Expose defines information for Redis nodePorts settings + // +optional + Expose InstanceAccess `json:"expose,omitempty"` + // Exporter defines Redis exporter settings + // +optional + Exporter *redisfailoverv1.RedisExporter `json:"exporter,omitempty"` + // EnableTLS enables TLS for Redis + // +optional + EnableTLS bool `json:"enableTLS,omitempty"` + // IPFamilyPrefer sets the preferable IP family for the Pod and Redis + // IPFamily represents the IP Family (IPv4 or IPv6). This type is used to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). + // +kubebuilder:validation:Enum="IPv4";"IPv6";"" + IPFamilyPrefer corev1.IPFamily `json:"ipFamilyPrefer,omitempty"` + // Pause field indicates whether Redis is paused. + // +optional + Pause bool `json:"pause,omitempty"` + + // Sentinel defines Sentinel configuration settings Sentinel + // +optional + Sentinel *redisfailoverv1.SentinelSettings `json:"sentinel,omitempty"` + // Backup holds information for Redis backups + Backup core.RedisBackup `json:"backup,omitempty"` + // Restore contains information for Redis + Restore core.RedisRestore `json:"restore,omitempty"` + + // EnableActiveRedis enable active-active model for Redis + EnableActiveRedis bool `json:"enableActiveRedis,omitempty"` + // ServiceID the service id for activeredis + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=15 + ServiceID *int32 `json:"serviceID,omitempty"` + + // UpgradeOption defines the upgrade strategy for the Redis instance. + UpgradeOption *UpgradeOption `json:"upgradeOption,omitempty"` + // Provides the ability to patch the generated manifest of several child resources. + Patches *RedisPatchSpec `json:"patches,omitempty"` +} + +// RedisPersistent defines the storage of Redis +type RedisPersistent struct { + // This field specifies the name of the storage class that should be used for the persistent storage of Redis + // +kubebuilder:validation:Required + StorageClassName string `json:"storageClassName"` +} + +// RedisReplicas defines the replicas of Redis +type RedisReplicas struct { + // This field specifies the number of replicas for Redis Cluster + Cluster *ClusterReplicas `json:"cluster,omitempty"` + // This field specifies the number of replicas for Redis sentinel + Sentinel *SentinelReplicas `json:"sentinel,omitempty"` +} + +type ClusterReplicas struct { + // This field specifies the number of master in Redis Cluster. + // +kubebuilder:validation:Minimum=3 + Shard *int32 `json:"shard"` + // This field specifies the number of replica nodes per Redis Cluster master. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=5 + Slave *int32 `json:"slave,omitempty"` + // This field specifies the assignment of cluster shard slots. + // this config is only works for new create instance, update will not take effect after instance is startup + Shards []clusterv1.ClusterShardConfig `json:"shards,omitempty"` +} + +type SentinelReplicas struct { + // sentinel master nodes, only 1 + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=1 + Master *int32 `json:"master"` + // This field specifies the number of replica nodes. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=5 + Slave *int32 `json:"slave,omitempty"` +} + +// RedisStatus defines the observed state of Redis +type RedisStatus struct { + // Phase indicates whether all the resource for the instance is ok. + // Values are as below: + // Initializing - Resource is in Initializing or Reconcile + // Ready - All resources is ok. In most cases, Ready means the cluster is ok to use + // Error - Error found when do resource init + Phase RedisPhase `json:"phase,omitempty"` + // This field contains an additional message for the instance's status + Message string `json:"message,omitempty"` + // The name of the kubernetes Secret that contains Redis password. + PasswordSecretName string `json:"passwordSecretName,omitempty"` + // The name of the kubernetes Service for Redis + ServiceName string `json:"serviceName,omitempty"` + // Matching labels selector for Redis + MatchLabels map[string]string `json:"matchLabels,omitempty"` + // Matching label selector for Redis proxy. + ProxyMatchLabels map[string]string `json:"proxyMatchLabels,omitempty"` + // The name of the kubernetes Service for Redis Proxy + ProxyServiceName string `json:"proxyServiceName,omitempty"` + // ClusterNodes redis nodes info + ClusterNodes []core.RedisNode `json:"clusterNodes,omitempty"` + // Restored indicates whether the instance has been restored from a backup. + // if the instance is set to restore from a backup, when the restore is completed, the restored field will be set to true. + Restored bool `json:"restored,omitempty"` + // LastShardCount indicates the last number of shards in the Redis Cluster. + LastShardCount int32 `json:"lastShardCount,omitempty"` + // LastVersion indicates the last version of the Redis instance. + LastVersion string `json:"lastVersion,omitempty"` + + // UpgradeStatus indicates the status of the bundle upgrade. + UpgradeStatus UpgradeStatus `json:"upgradeStatus,omitempty"` + // DetailedStatusRef detailed status resource ref + DetailedStatusRef *corev1.ObjectReference `json:"detailedStatusRef,omitempty"` +} + +// RedisPhase +type RedisPhase string + +const ( + // Initializing + RedisPhaseInit RedisPhase = "Initializing" + // Rebalancing + RedisPhaseRebalancing RedisPhase = "Rebalancing" + // Ready + RedisPhaseReady RedisPhase = "Ready" + // Error + RedisPhaseError RedisPhase = "Error" + // Paused + RedisPhasePaused RedisPhase = "Paused" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Arch",type="string",JSONPath=".spec.arch",description="Instance arch" +// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.version",description="Redis version" +// +kubebuilder:printcolumn:name="Access",type="string",JSONPath=".spec.expose.type",description="Instance access type" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Instance phase" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message",description="Instance status message" +// +kubebuilder:printcolumn:name="Bundle Version",type="string",JSONPath=".status.upgradeStatus.crVersion",description="Bundle Version" +// +kubebuilder:printcolumn:name="AutoUpgrade",type="boolean",JSONPath=".spec.upgradeOption.autoUpgrade",description="Enable instance auto upgrade" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since creation" + +// Redis is the Schema for the redis API +type Redis struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisSpec `json:"spec,omitempty"` + Status RedisStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RedisList contains a list of Redis +type RedisList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Redis `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Redis{}, &RedisList{}) +} + +func (r *Redis) PasswordIsEmpty() bool { + return len(r.Spec.PasswordSecret) == 0 +} + +func (r *Redis) SetStatusInit() { + r.Status.Phase = RedisPhaseInit +} + +func (r *Redis) SetStatusReady() { + r.Status.Phase = RedisPhaseReady + r.Status.Message = "" +} + +func (r *Redis) SetStatusPaused() { + r.Status.Phase = RedisPhasePaused + r.Status.Message = "" +} + +func (r *Redis) SetStatusRebalancing(msg string) { + r.Status.Phase = RedisPhaseRebalancing + r.Status.Message = msg +} + +func (r *Redis) SetStatusUnReady(msg string) { + r.Status.Phase = RedisPhaseInit + r.Status.Message = msg +} + +func (r *Redis) SetStatusError(msg string) { + r.Status.Phase = RedisPhaseError + r.Status.Message = msg +} + +func (r *Redis) RecoverStatusError() { + if r.Status.Phase == RedisPhaseError { + r.Status.Phase = RedisPhaseInit + } + // r.Status.Message = "" +} + +type RedisProxy struct { + // a boolean indicating whether or not the Redis Proxy service is enabled. + Enable bool `json:"enable,omitempty"` + // a string representing the Docker image to use for the proxy. + Image string `json:"image,omitempty"` + // an integer indicating the number of replicas to create for the proxy. + Replicas int32 `json:"replicas,omitempty"` + // a map holding additional configuration options for the proxy. + Config map[string]string `json:"config,omitempty"` + // Resources holds ResourceRequirements for the MySQL Agent & Server Containers + // +optional + Resources *v1.ResourceRequirements `json:"resources,omitempty"` + // Tolerations allows specifying a list of tolerations for controlling which + // set of Nodes a Pod can be scheduled on + // +optional + Tolerations []v1.Toleration `json:"tolerations,omitempty"` + // NodeSelector is a selector which must be true for the pod to fit on a node. + // Selector which must match a node's labels for the pod to be scheduled on that node. + // More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // If specified, affinity will define the pod's scheduling constraints + // +optional + Affinity *v1.Affinity `json:"affinity,omitempty"` +} + +func (r *Redis) SetPasswordSecret(secretName string) { + r.Status.PasswordSecretName = secretName +} + +func (r *Redis) SetServiceName(serviceName string) { + r.Status.ServiceName = serviceName +} + +func (r *Redis) SetMatchLabels(labels map[string]string) { + r.Status.MatchLabels = labels +} + +// Provides the ability to patch the generated manifest of several child resources. +type RedisPatchSpec struct { + // Patch configuration for the Service created to serve traffic to the cluster. + Services []*Service `json:"services,omitempty"` +} + +// EmbeddedObjectMeta is an embedded subset of the fields included in k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta. +// Only fields which are relevant to embedded resources are included. +type EmbeddedObjectMeta struct { + // Name must be unique within a namespace. Is required when creating resources, although + // some resources may allow a client to request the generation of an appropriate name + // automatically. Name is primarily intended for creation idempotence and configuration + // definition. + // Cannot be updated. + // More info: http://kubernetes.io/docs/user-guide/identifiers#names + // +optional + Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` + + // Namespace defines the space within each name must be unique. An empty namespace is + // equivalent to the "default" namespace, but "default" is the canonical representation. + // Not all objects are required to be scoped to a namespace - the value of this field for + // those objects will be empty. + // + // Must be a DNS_LABEL. + // Cannot be updated. + // More info: http://kubernetes.io/docs/user-guide/namespaces + // +optional + Namespace string `json:"namespace,omitempty" protobuf:"bytes,3,opt,name=namespace"` + + // Map of string keys and values that can be used to organize and categorize + // (scope and select) objects. May match selectors of replication controllers + // and services. + // More info: http://kubernetes.io/docs/user-guide/labels + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"` + + // Annotations is an unstructured key value map stored with a resource that may be + // set by external tools to store and retrieve arbitrary metadata. They are not + // queryable and should be preserved when modifying objects. + // More info: http://kubernetes.io/docs/user-guide/annotations + // +optional + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"` +} + +// Patch configuration for the Service created to serve traffic to the cluster. +// Allows for the manifest of the created Service to be overwritten with custom configuration. +type Service struct { + // +optional + *EmbeddedObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec defines the behavior of a Service. + // https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +optional + Spec v1.ServiceSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} diff --git a/api/middleware/v1/redis_webhook.go b/api/middleware/v1/redis_webhook.go new file mode 100644 index 0000000..1aac885 --- /dev/null +++ b/api/middleware/v1/redis_webhook.go @@ -0,0 +1,698 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "os" + "slices" + "strconv" + "strings" + "time" + + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + corehelper "github.com/alauda/redis-operator/api/core/helper" + coreVal "github.com/alauda/redis-operator/api/core/validation" + redisfailoverv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/api/middleware/v1/helper" + "github.com/alauda/redis-operator/api/middleware/v1/validation" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/pkg/slot" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/samber/lo" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +const ( + redisRestartAnnotation = "kubectl.kubernetes.io/restartedAt" + PauseAnnotationKey = "app.cpaas.io/pause-timestamp" + RedisClusterPVCSizeAnnotation = "middleware.alauda.io/storage_size" + + ConfigReplBacklogSizeKey = "repl-backlog-size" +) + +// log is for logging in this package. +var logger = logf.Log.WithName("redis-webhook") +var mgrClient client.Client + +func (r *Redis) SetupWebhookWithManager(mgr ctrl.Manager) error { + mgrClient = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:verbs=create;update,path=/mutate-middleware-alauda-io-v1-redis,mutating=true,failurePolicy=fail,groups=middleware.alauda.io,resources=redis,versions=v1,name=mredis.kb.io,sideEffects=none,admissionReviewVersions=v1 +//+kubebuilder:webhook:verbs=create;update;delete,path=/validate-middleware-alauda-io-v1-redis,mutating=false,failurePolicy=fail,groups=middleware.alauda.io,resources=redis,versions=v1,name=vredis.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var _ webhook.Validator = (*Redis)(nil) +var _ webhook.Defaulter = (*Redis)(nil) + +func (r *Redis) Default() { + if r.Annotations == nil { + r.Annotations = make(map[string]string) + } + if r.Spec.CustomConfig == nil { + r.Spec.CustomConfig = make(map[string]string) + } + if r.Spec.PodAnnotations == nil { + r.Spec.PodAnnotations = make(map[string]string) + } + if r.Spec.UpgradeOption == nil { + r.Spec.UpgradeOption = &UpgradeOption{} + } + + // TODO: apply this config to the innner redis instance + if r.Spec.CustomConfig[ConfigReplBacklogSizeKey] == "" { + // https://raw.githubusercontent.com/antirez/redis/7.0/redis.conf + // https://docs.redis.com/latest/rs/databases/active-active/manage/#replication-backlog + if r.Spec.Resources != nil { + for _, resource := range []*resource.Quantity{r.Spec.Resources.Limits.Memory(), r.Spec.Resources.Requests.Memory()} { + if resource == nil { + continue + } + if val, ok := resource.AsInt64(); ok { + val = int64(0.01 * float64(val)) + if val > 256*1024*1024 { + val = 256 * 1024 * 1024 + } else if val < 1024*1024 { + val = 1024 * 1024 + } + r.Spec.CustomConfig[ConfigReplBacklogSizeKey] = fmt.Sprintf("%d", val) + } + } + } + } + + ver := redis.RedisVersion(r.Spec.Version) + if rename := r.Spec.CustomConfig["rename-command"]; !ver.IsACLSupported() || rename != "" { + r.Spec.CustomConfig["rename-command"] = helper.BuildRenameCommand(rename) + } + + if r.Spec.Pause { + if _, ok := r.Spec.PodAnnotations[PauseAnnotationKey]; !ok { + r.Spec.PodAnnotations[PauseAnnotationKey] = metav1.NewTime(time.Now()).Format(time.RFC3339) + } + } else { + delete(r.Spec.PodAnnotations, PauseAnnotationKey) + } + + // reset expose image + if r.Spec.Expose.EnableNodePort && r.Spec.Expose.ServiceType == "" { + r.Spec.Expose.ServiceType = v1.ServiceTypeNodePort + } + if r.Spec.Expose.ServiceType != v1.ServiceTypeNodePort { + r.Spec.Expose.EnableNodePort = false + } + if r.Spec.IPFamilyPrefer == "" { + r.Spec.IPFamilyPrefer = corehelper.GetDefaultIPFamily(os.Getenv("POD_IP")) + } + + // init exporter settings + if r.Spec.Exporter == nil { + r.Spec.Exporter = &redisfailoverv1.RedisExporter{Enabled: true} + } + if r.Spec.Exporter.Resources.Limits.Cpu().IsZero() || + r.Spec.Exporter.Resources.Limits.Memory().IsZero() { + r.Spec.Exporter.Resources = v1.ResourceRequirements{ + Requests: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("50m"), + v1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("384Mi"), + }, + } + } + + if r.Spec.Persistent != nil && r.Spec.Persistent.StorageClassName != "" && + (r.Spec.PersistentSize == nil || r.Spec.PersistentSize.IsZero()) { + size := resource.NewQuantity(r.Spec.Resources.Limits.Memory().Value()*2, resource.BinarySI) + r.Spec.PersistentSize = size + } + + // init pvc annotations + if r.Spec.PersistentSize != nil { + if val, _ := r.Spec.PersistentSize.AsInt64(); val > 0 { + if r.Spec.Arch == core.RedisCluster { + storageConfig := map[int32]string{} + storageConfigVal := r.Annotations[RedisClusterPVCSizeAnnotation] + if storageConfigVal != "" { + if err := json.Unmarshal([]byte(storageConfigVal), &storageConfig); err != nil { + logger.Error(err, "failed to unmarshal storage size") + } + } + if r.Spec.Replicas != nil && r.Spec.Replicas.Cluster != nil && r.Spec.Replicas.Cluster.Shard != nil { + for i := int32(0); i < *r.Spec.Replicas.Cluster.Shard; i++ { + if val := storageConfig[i]; val == "" { + storageConfig[i] = r.Spec.PersistentSize.String() + } + } + } + data, _ := json.Marshal(storageConfig) + r.Annotations[RedisClusterPVCSizeAnnotation] = string(data) + } + } + } + + switch r.Spec.Arch { + case core.RedisSentinel: + r.Spec.RedisProxy = nil + if r.Spec.Replicas == nil { + r.Spec.Replicas = &RedisReplicas{} + } + if r.Spec.Replicas.Sentinel == nil { + r.Spec.Replicas.Sentinel = &SentinelReplicas{} + } + if r.Spec.Sentinel == nil { + r.Spec.Sentinel = &redisfailoverv1.SentinelSettings{} + } + if r.Spec.Sentinel.SentinelReference == nil { + if r.Spec.Replicas.Sentinel.Master == nil || *r.Spec.Replicas.Sentinel.Master != 1 { + r.Spec.Replicas.Sentinel.Master = pointer.Int32(1) + } + if r.Spec.Sentinel.Replicas <= 0 { + r.Spec.Sentinel.Replicas = 3 + } + if len(r.Spec.SentinelCustomConfig) != 0 { + r.Spec.Sentinel.MonitorConfig = r.Spec.SentinelCustomConfig + r.Spec.SentinelCustomConfig = nil + } + + sentinel := r.Spec.Sentinel + if r.Annotations["createType"] != "managerView" && + sentinel.Resources.Limits.Cpu().IsZero() && sentinel.Resources.Limits.Memory().IsZero() { + sentinel.Resources = v1.ResourceRequirements{ + Requests: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("128Mi"), + }, + } + } + + r.Spec.Sentinel.Expose.ServiceType = r.Spec.Expose.ServiceType + // reset pod assignment + r.Spec.Sentinel.Expose.AccessPort = r.Spec.Expose.AccessPort + if r.Spec.Expose.ServiceType == v1.ServiceTypeNodePort { + if len(r.Spec.Expose.NodePortMap) != 0 { + items := lo.MapToSlice(r.Spec.Expose.NodePortMap, func(k string, v int32) [2]int32 { + ik, _ := strconv.ParseInt(k, 10, 32) + return [2]int32{int32(ik), v} + }) + slices.SortStableFunc(items, func(i, j [2]int32) int { + if i[0] < j[0] { + return -1 + } + return 1 + }) + ports := lo.Map(items, func(item [2]int32, i int) string { + return fmt.Sprintf("%d", item[1]) + }) + r.Spec.Expose.NodePortSequence = strings.Join(ports, ",") + // TODO: deprecate NodePortMap in 3.22, for 3.14 use this field + // r.Spec.Expose.NodePortMap = nil + } + } + } + case core.RedisStandalone: + if r.Spec.Replicas == nil { + r.Spec.Replicas = &RedisReplicas{} + } + if r.Spec.Replicas.Sentinel == nil { + r.Spec.Replicas.Sentinel = &SentinelReplicas{} + } + r.Spec.Replicas.Sentinel.Master = pointer.Int32(1) + r.Spec.Replicas.Sentinel.Slave = nil + r.Spec.RedisProxy = nil + r.Spec.Sentinel = nil + r.Spec.SentinelCustomConfig = nil + + if r.Spec.Expose.ServiceType == v1.ServiceTypeNodePort { + if len(r.Spec.Expose.NodePortMap) != 0 { + items := lo.MapToSlice(r.Spec.Expose.NodePortMap, func(k string, v int32) [2]int32 { + ik, _ := strconv.ParseInt(k, 10, 32) + return [2]int32{int32(ik), v} + }) + slices.SortStableFunc(items, func(i, j [2]int32) int { + if i[0] < j[0] { + return -1 + } + return 1 + }) + ports := lo.Map(items, func(item [2]int32, i int) string { + return fmt.Sprintf("%d", item[1]) + }) + r.Spec.Expose.NodePortSequence = strings.Join(ports, ",") + // TODO: deprecate NodePortMap in 3.22, for 3.14 use this field + // r.Spec.Expose.NodePortMap = nil + } + } + } + + if r.Spec.UpgradeOption == nil || r.Spec.UpgradeOption.AutoUpgrade == nil || *r.Spec.UpgradeOption.AutoUpgrade { + delete(r.Annotations, config.CRUpgradeableVersion) + delete(r.Annotations, config.CRUpgradeableComponentVersion) + r.Spec.UpgradeOption.CRVersion = "" + } + + // Patch + if r.Spec.Arch == core.RedisSentinel { + needCheckAff := r.Spec.UpgradeOption == nil || r.Spec.UpgradeOption.AutoUpgrade == nil || *r.Spec.UpgradeOption.AutoUpgrade + if r.Spec.UpgradeOption != nil && + !strings.HasPrefix(r.Spec.UpgradeOption.CRVersion, "3.14") && + !strings.HasPrefix(r.Spec.UpgradeOption.CRVersion, "3.15") && + !strings.HasPrefix(r.Spec.UpgradeOption.CRVersion, "3.16") && + !strings.HasPrefix(r.Spec.UpgradeOption.CRVersion, "3.17") { + needCheckAff = true + } + + // fix sentinel affinity error + if needCheckAff { + labels := []string{ + "redissentinels.databases.spotahome.com/name", // NOTE: keep this key as the first + "middleware.instance/name", + "app.kubernetes.io/name", + } + isLabelSpecified := func(term v1.PodAffinityTerm) bool { + if term.LabelSelector == nil { + return false + } + for _, exp := range term.LabelSelector.MatchExpressions { + if slices.Contains(labels, exp.Key) && + exp.Operator == metav1.LabelSelectorOpIn && + slices.Contains(exp.Values, r.Name) { + return true + } + } + for k, v := range term.LabelSelector.MatchLabels { + if slices.Contains(labels, k) && v == r.Name { + return true + } + } + return false + } + + if r.Spec.Sentinel.Affinity == nil { + r.Spec.Sentinel.Affinity = &v1.Affinity{ + PodAntiAffinity: &v1.PodAntiAffinity{}, + } + } + if r.Spec.Sentinel.Affinity.PodAntiAffinity == nil { + r.Spec.Sentinel.Affinity.PodAntiAffinity = &v1.PodAntiAffinity{} + } + + antiAff := r.Spec.Sentinel.Affinity.PodAntiAffinity + if len(antiAff.RequiredDuringSchedulingIgnoredDuringExecution) == 0 { + foundLabelMatch := false + for _, item := range antiAff.PreferredDuringSchedulingIgnoredDuringExecution { + if flag := isLabelSpecified(item.PodAffinityTerm); flag { + foundLabelMatch = true + break + } + } + if !foundLabelMatch { + term := v1.PodAffinityTerm{ + TopologyKey: "kubernetes.io/hostname", + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: labels[0], + Operator: metav1.LabelSelectorOpIn, + Values: []string{r.Name}, + }, + { + Key: "app.kubernetes.io/component", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"sentinel"}, + }, + }, + }, + } + antiAff.PreferredDuringSchedulingIgnoredDuringExecution = []v1.WeightedPodAffinityTerm{ + {Weight: 100, PodAffinityTerm: term}, + } + } + } else { + foundLabelMatch := false + for _, term := range antiAff.RequiredDuringSchedulingIgnoredDuringExecution { + if flag := isLabelSpecified(term); flag { + foundLabelMatch = true + break + } + } + if !foundLabelMatch { + term := v1.PodAffinityTerm{ + TopologyKey: "kubernetes.io/hostname", + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: labels[0], + Operator: metav1.LabelSelectorOpIn, + Values: []string{r.Name}, + }, + { + Key: "app.kubernetes.io/component", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"sentinel"}, + }, + }, + }, + } + antiAff.RequiredDuringSchedulingIgnoredDuringExecution = []v1.PodAffinityTerm{term} + } + } + } + } +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Redis) ValidateCreate() (warns admission.Warnings, err error) { + if err := validation.ValidatePasswordSecret(r.Namespace, r.Spec.PasswordSecret, mgrClient, &warns); err != nil { + return warns, err + } + + if err := coreVal.ValidateInstanceAccess(&r.Spec.Expose.InstanceAccessBase, getNodeCountByArch(r.Spec.Arch, r.Spec.Replicas), &warns); err != nil { + return warns, err + } + + switch r.Spec.Arch { + case core.RedisCluster: + if r.Spec.Replicas == nil || r.Spec.Replicas.Cluster == nil || r.Spec.Replicas.Cluster.Shard == nil { + return nil, fmt.Errorf("instance replicas not specified") + } + shards := int32(len(r.Spec.Replicas.Cluster.Shards)) + if shards > 0 { + if shards != *r.Spec.Replicas.Cluster.Shard { + return nil, fmt.Errorf("specified shard slots list length not not match shards count") + } + var ( + fullSlots *slot.Slots + total int + ) + for _, shard := range r.Spec.Replicas.Cluster.Shards { + if shardSlots, err := slot.LoadSlots(shard.Slots); err != nil { + return nil, fmt.Errorf("failed to load shard slots: %v", err) + } else { + fullSlots = fullSlots.Union(shardSlots) + total += shardSlots.Count(slot.SlotAssigned) + } + } + if !fullSlots.IsFullfilled() { + return nil, fmt.Errorf("specified shard slots not fullfilled") + } + if total > slot.RedisMaxSlots { + return nil, fmt.Errorf("specified shard slots duplicated") + } + } + case core.RedisSentinel: + if r.Spec.Replicas == nil || r.Spec.Replicas.Sentinel == nil { + return nil, fmt.Errorf("instance replicas not specified") + } + if r.Spec.Replicas.Sentinel == nil || r.Spec.Replicas.Sentinel.Master == nil || *r.Spec.Replicas.Sentinel.Master != 1 { + return nil, fmt.Errorf("spec.replicas.sentinel.master must be 1") + } + if r.Spec.Sentinel == nil { + return nil, fmt.Errorf("spec.sentinel not specified") + } + if r.Spec.Sentinel.Replicas != 0 && (r.Spec.Sentinel.Replicas%2 == 0 || r.Spec.Sentinel.Replicas < 3) { + return nil, fmt.Errorf("sentinel replicas must be odd and greater >= 3") + } + if r.Spec.Sentinel.PasswordSecret != "" { + if err := validation.ValidatePasswordSecret(r.Namespace, r.Spec.Sentinel.PasswordSecret, mgrClient, &warns); err != nil { + return warns, fmt.Errorf("sentinel password secret: %v", err) + } + } + + if err := coreVal.ValidateInstanceAccess(&r.Spec.Sentinel.Expose.InstanceAccessBase, int(r.Spec.Sentinel.Replicas), &warns); err != nil { + return warns, err + } + + if r.Spec.Expose.ServiceType == v1.ServiceTypeNodePort { + portMap := map[int32]struct{}{} + if port := r.Spec.Sentinel.Expose.AccessPort; port != 0 { + portMap[port] = struct{}{} + } + ports, _ := corehelper.ParseSequencePorts(r.Spec.Expose.NodePortSequence) + for _, port := range ports { + if _, ok := portMap[port]; ok { + return nil, fmt.Errorf("port %d has assigned to spec.sentinel.expose.accessPort", port) + } + portMap[port] = struct{}{} + } + ports, _ = corehelper.ParseSequencePorts(r.Spec.Sentinel.Expose.NodePortSequence) + for _, port := range ports { + if _, ok := portMap[port]; ok { + if port == r.Spec.Sentinel.Expose.AccessPort { + return nil, fmt.Errorf("port %d has assigned to spec.sentinel.expose.accessPort", port) + } + return nil, fmt.Errorf("port %d has assigned to spec.expose.dataStorageNodePortSequence", port) + } + } + } + case core.RedisStandalone: + if r.Spec.Replicas == nil || r.Spec.Replicas.Sentinel == nil || r.Spec.Replicas.Sentinel.Master == nil || *r.Spec.Replicas.Sentinel.Master != 1 { + return nil, fmt.Errorf("spec.replicas.sentinel.master must be 1") + } + r.Spec.Replicas.Sentinel.Slave = nil + } + + if err := validation.ValidateActiveRedisService(r.Spec.EnableActiveRedis, r.Spec.ServiceID, &warns); err != nil { + return warns, err + } + return +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Redis) ValidateUpdate(_ runtime.Object) (warns admission.Warnings, err error) { + if err := validation.ValidatePasswordSecret(r.Namespace, r.Spec.PasswordSecret, mgrClient, &warns); err != nil { + return warns, err + } + + switch r.Spec.Arch { + case core.RedisCluster: + if r.Spec.Replicas == nil || r.Spec.Replicas.Cluster == nil || r.Spec.Replicas.Cluster.Shard == nil { + return nil, fmt.Errorf("instance replicas not specified") + } + + if err := coreVal.ValidateInstanceAccess(&r.Spec.Expose.InstanceAccessBase, + helper.CalculateNodeCount(r.Spec.Arch, r.Spec.Replicas.Cluster.Shard, r.Spec.Replicas.Cluster.Slave), + &warns); err != nil { + return warns, err + } + + shards := int32(len(r.Spec.Replicas.Cluster.Shards)) + if shards > 0 && shards == *r.Spec.Replicas.Cluster.Shard { + // for update validator, only check slots fullfilled + var ( + fullSlots *slot.Slots + total int + ) + for _, shard := range r.Spec.Replicas.Cluster.Shards { + if shardSlots, err := slot.LoadSlots(shard.Slots); err != nil { + return nil, fmt.Errorf("failed to load shard slots: %v", err) + } else { + fullSlots = fullSlots.Union(shardSlots) + total += shardSlots.Count(slot.SlotAssigned) + } + } + if !fullSlots.IsFullfilled() { + return nil, fmt.Errorf("specified shard slots not fullfilled") + } + if total > slot.RedisMaxSlots { + return nil, fmt.Errorf("specified shard slots duplicated") + } + } + + if r.Status.DetailedStatusRef != nil && r.Status.DetailedStatusRef.Name != "" { + var ( + detailedStatus clusterv1.DistributedRedisClusterDetailedStatus + detailedStatusCM v1.ConfigMap + ) + if err := mgrClient.Get(context.TODO(), client.ObjectKey{ + Namespace: r.Namespace, + Name: r.Status.DetailedStatusRef.Name, + }, &detailedStatusCM); errors.IsNotFound(err) { + } else if err != nil { + return nil, err + } else { + if err := json.Unmarshal([]byte(detailedStatusCM.Data["status"]), &detailedStatus); err != nil { + return nil, err + } + // validate resource scaling + dataset := helper.ExtractShardDatasetUsedMemory(r.Name, int(r.Status.LastShardCount), detailedStatus.Nodes) + if err := validation.ValidateClusterScalingResource(*r.Spec.Replicas.Cluster.Shard, r.Spec.Resources, dataset, &warns); err != nil { + return warns, err + } + } + } + case core.RedisSentinel: + if r.Spec.Replicas == nil || r.Spec.Replicas.Sentinel == nil { + return nil, fmt.Errorf("instance replicas not specified") + } + if r.Spec.Replicas.Sentinel == nil || r.Spec.Replicas.Sentinel.Master == nil || *r.Spec.Replicas.Sentinel.Master != 1 { + return nil, fmt.Errorf("spec.replicas.sentinel.master must be 1") + } + if r.Spec.Sentinel.Replicas != 0 && (r.Spec.Sentinel.Replicas%2 == 0 || r.Spec.Sentinel.Replicas < 3) { + return nil, fmt.Errorf("sentinel replicas must be odd and greater >= 3") + } + + if err := coreVal.ValidateInstanceAccess(&r.Spec.Expose.InstanceAccessBase, + helper.CalculateNodeCount(r.Spec.Arch, r.Spec.Replicas.Sentinel.Master, r.Spec.Replicas.Sentinel.Slave), + &warns); err != nil { + return warns, err + } + if err := coreVal.ValidateInstanceAccess(&r.Spec.Sentinel.Expose.InstanceAccessBase, int(r.Spec.Sentinel.Replicas), &warns); err != nil { + return warns, err + } + + if r.Spec.Expose.ServiceType == v1.ServiceTypeNodePort { + portMap := map[int32]struct{}{} + if port := r.Spec.Sentinel.Expose.AccessPort; port != 0 { + portMap[port] = struct{}{} + } + ports, _ := corehelper.ParseSequencePorts(r.Spec.Expose.NodePortSequence) + for _, port := range ports { + if _, ok := portMap[port]; ok { + return nil, fmt.Errorf("port %d has assigned to spec.sentinel.expose.accessPort", port) + } + portMap[port] = struct{}{} + } + ports, _ = corehelper.ParseSequencePorts(r.Spec.Sentinel.Expose.NodePortSequence) + for _, port := range ports { + if _, ok := portMap[port]; ok { + if port == r.Spec.Sentinel.Expose.AccessPort { + return nil, fmt.Errorf("port %d has assigned to spec.sentinel.expose.accessPort", port) + } + return nil, fmt.Errorf("port %d has assigned to spec.expose.dataStorageNodePortSequence", port) + } + } + } + + if r.Status.DetailedStatusRef != nil && r.Status.DetailedStatusRef.Name != "" { + var ( + detailedStatus redisfailoverv1.RedisFailoverDetailedStatus + detailedStatusCM v1.ConfigMap + ) + if err := mgrClient.Get(context.TODO(), client.ObjectKey{ + Namespace: r.Namespace, + Name: r.Status.DetailedStatusRef.Name, + }, &detailedStatusCM); errors.IsNotFound(err) { + } else if err != nil { + return nil, err + } else { + if err := json.Unmarshal([]byte(detailedStatusCM.Data["status"]), &detailedStatus); err != nil { + return nil, err + } + // validate resource scaling + dataset := helper.ExtractShardDatasetUsedMemory(r.Name, 1, detailedStatus.Nodes) + if err := validation.ValidateReplicationScalingResource(r.Spec.Resources, dataset[0], &warns); err != nil { + return warns, err + } + } + } + case core.RedisStandalone: + if r.Spec.Replicas == nil || r.Spec.Replicas.Sentinel == nil || + r.Spec.Replicas.Sentinel.Master == nil || *r.Spec.Replicas.Sentinel.Master != 1 { + return nil, fmt.Errorf("spec.replicas.sentinel.master must be 1") + } + + if err := coreVal.ValidateInstanceAccess(&r.Spec.Expose.InstanceAccessBase, + helper.CalculateNodeCount(r.Spec.Arch, r.Spec.Replicas.Sentinel.Master, r.Spec.Replicas.Sentinel.Slave), + &warns); err != nil { + return warns, err + } + + if r.Status.DetailedStatusRef != nil && r.Status.DetailedStatusRef.Name != "" { + var ( + detailedStatus redisfailoverv1.RedisFailoverDetailedStatus + detailedStatusCM v1.ConfigMap + ) + if err := mgrClient.Get(context.TODO(), client.ObjectKey{ + Namespace: r.Namespace, + Name: r.Status.DetailedStatusRef.Name, + }, &detailedStatusCM); errors.IsNotFound(err) { + } else if err != nil { + return nil, err + } else { + if err := json.Unmarshal([]byte(detailedStatusCM.Data["status"]), &detailedStatus); err != nil { + return nil, err + } + // validate resource scaling + dataset := helper.ExtractShardDatasetUsedMemory(r.Name, 1, detailedStatus.Nodes) + if err := validation.ValidateReplicationScalingResource(r.Spec.Resources, dataset[0], &warns); err != nil { + return warns, err + } + } + } + } + + if err := validation.ValidateActiveRedisService(r.Spec.EnableActiveRedis, r.Spec.ServiceID, &warns); err != nil { + return warns, err + } + return +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Redis) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} + +func getNodeCountByArch(arch core.Arch, replicas *RedisReplicas) int { + if replicas == nil { + return 0 + } + + switch arch { + case core.RedisCluster: + if replicas.Cluster == nil || replicas.Cluster.Shard == nil { + return 0 + } + if replicas.Cluster != nil && replicas.Cluster.Slave != nil { + return int(*replicas.Cluster.Shard) * int((*replicas.Cluster.Slave)+1) + } + return int(*replicas.Cluster.Shard) + case core.RedisSentinel: + if replicas.Sentinel == nil { + return 0 + } + if replicas.Sentinel.Slave != nil { + return int(*replicas.Sentinel.Slave) + 1 + } + return 1 + case core.RedisStandalone: + return 1 + default: + return 0 + } +} diff --git a/api/middleware/v1/redis_webhook_test.go b/api/middleware/v1/redis_webhook_test.go new file mode 100644 index 0000000..7f941be --- /dev/null +++ b/api/middleware/v1/redis_webhook_test.go @@ -0,0 +1,3510 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v1 + +import ( + "encoding/json" + "fmt" + "reflect" + "slices" + "strings" + "testing" + + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + redisfailoverv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestRedis_Default(t *testing.T) { + tests := []struct { + name string + data string + want string + }{ + { + name: "3.14 enable nodeport", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: {} + exporter: + enabled: true + expose: + enableNodePort: true + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - redis + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + crVersion: 3.14.50 + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + enableNodePort: true + type: NodePort + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - redis + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + crVersion: 3.14.50 + version: "6.0"`, + }, + { + name: "3.14 enable nodeport with custom port", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + enableNodePort: true + accessPort: 31002 + dataStorageNodePortMap: + "0": 31000 + "1": 31001 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - redis + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + crVersion: 3.14.50 + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + enableNodePort: true + type: NodePort + accessPort: 31002 + dataStorageNodePortMap: + "0": 31000 + "1": 31001 + dataStorageNodePortSequence: 31000,31001 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + accessPort: 31002 + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - redis + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + crVersion: 3.14.50 + version: "6.0"`, + }, + { + name: "3.18 enable nodeport", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: {} + exporter: + enabled: true + expose: + type: NodePort + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - redis + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redissentinels.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - sentinel + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + version: "6.0"`, + }, + { + name: "3.18 enable nodeport with custom port", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + accessPort: 31002 + dataStorageNodePortMap: + "0": 31000 + "1": 31001 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - redis + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + accessPort: 31002 + dataStorageNodePortMap: + "0": 31000 + "1": 31001 + dataStorageNodePortSequence: 31000,31001 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + accessPort: 31002 + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: redissentinels.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - sentinel + topologyKey: kubernetes.io/hostname + upgradeOption: + autoUpgrade: false + version: "6.0"`, + }, + { + name: "failover setup default affinity", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + accessPort: 31002 + dataStorageNodePortMap: + "0": 31000 + "1": 31001 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: [] + upgradeOption: + autoUpgrade: false + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + accessPort: 31002 + dataStorageNodePortMap: + "0": 31000 + "1": 31001 + dataStorageNodePortSequence: 31000,31001 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + sentinelCustomConfig: {} + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + accessPort: 31002 + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: redissentinels.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + - key: app.kubernetes.io/component + operator: In + values: + - sentinel + topologyKey: kubernetes.io/hostname + weight: 100 + upgradeOption: + autoUpgrade: false + version: "6.0"`, + }, + { + name: "standalone setup default", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: standalone + backup: {} + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + dataStorageNodePortMap: + "0": 31000 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + upgradeOption: + autoUpgrade: false + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6-notupgrade-nodeport +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: standalone + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + dataStorageNodePortMap: + "0": 31000 + dataStorageNodePortSequence: "31000" + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + upgradeOption: + autoUpgrade: false + version: "6.0"`, + }, + { + name: "default rename config", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s5 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: standalone + backup: {} + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + dataStorageNodePortMap: + "0": 31000 + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + upgradeOption: + autoUpgrade: false + version: "5.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s5 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6-notupgrade-nodeport + topologyKey: kubernetes.io/hostname + arch: standalone + backup: {} + customConfig: + repl-backlog-size: "3145728" + rename-command: flushall "" flushdb "" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + dataStorageNodePortMap: + "0": 31000 + dataStorageNodePortSequence: "31000" + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + upgradeOption: + autoUpgrade: false + version: "5.0"`, + }, + { + name: "pause", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6 + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + expose: + type: NodePort + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + upgradeOption: + autoUpgrade: false + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6 + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + passwordSecret: redis-password-np + podAnnotations: + app.cpaas.io/pause-timestamp: "2024-08-15T16:24:01+08:00" + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: redissentinels.databases.spotahome.com/name + operator: In + values: + - s6 + - key: app.kubernetes.io/component + operator: In + values: + - sentinel + topologyKey: kubernetes.io/hostname + weight: 100 + upgradeOption: + autoUpgrade: false + version: "6.0"`, + }, + { + name: "update options", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + middleware.upgrade.crVersion: "3.18.0" + middleware.upgrade.component.version: "6.0.20" + name: s6 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6 + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + expose: + type: NodePort + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.instance/autoUpgrade: "false" + name: s6 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6 + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + passwordSecret: redis-password-np + podAnnotations: + app.cpaas.io/pause-timestamp: "2024-08-15T16:24:01+08:00" + persistent: + storageClassName: sc-topolvm + persistentSize: 1Gi + replicas: + sentinel: + master: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: redissentinels.databases.spotahome.com/name + operator: In + values: + - s6 + - key: app.kubernetes.io/component + operator: In + values: + - sentinel + topologyKey: kubernetes.io/hostname + weight: 100 + upgradeOption: {} + version: "6.0"`, + }, + { + name: "failover default persistent size", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.upgrade.crVersion: "3.18.0" + middleware.upgrade.component.version: "6.0.20" + name: s6 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6 + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + expose: + type: NodePort + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: {} + name: s6 +spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - redis + - key: redisfailovers.databases.spotahome.com/name + operator: In + values: + - s6 + topologyKey: kubernetes.io/hostname + arch: sentinel + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + passwordSecret: redis-password-np + podAnnotations: + app.cpaas.io/pause-timestamp: "2024-08-15T16:24:01+08:00" + persistent: + storageClassName: sc-topolvm + persistentSize: 600Mi + replicas: + sentinel: + master: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + expose: + type: NodePort + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: redissentinels.databases.spotahome.com/name + operator: In + values: + - s6 + - key: app.kubernetes.io/component + operator: In + values: + - sentinel + topologyKey: kubernetes.io/hostname + weight: 100 + upgradeOption: {} + version: "6.0"`, + }, + { + name: "cluster default persistent size", + data: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.upgrade.crVersion: "3.18.0" + middleware.upgrade.component.version: "6.0.20" + name: c6 +spec: + arch: cluster + backup: {} + replicas: + cluster: + shard: 3 + slave: 1 + expose: + type: NodePort + passwordSecret: redis-password-np + persistent: + storageClassName: sc-topolvm + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + version: "6.0"`, + want: `apiVersion: middleware.alauda.io/v1 +kind: Redis +metadata: + annotations: + middleware.alauda.io/storage_size: "{\"0\":\"600Mi\",\"1\":\"600Mi\",\"2\":\"600Mi\"}" + name: c6 +spec: + arch: cluster + backup: {} + customConfig: + repl-backlog-size: "3145728" + exporter: + enabled: true + resources: + limits: + cpu: 100m + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + expose: + type: NodePort + passwordSecret: redis-password-np + podAnnotations: + app.cpaas.io/pause-timestamp: "2024-08-15T16:24:01+08:00" + persistent: + storageClassName: sc-topolvm + persistentSize: 600Mi + replicas: + cluster: + shard: 3 + slave: 1 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 300m + memory: 300Mi + pause: true + upgradeOption: {} + version: "6.0"`, + }, + } + + var fieldDiffCheck func(t *testing.T, r, w any) (bool, any, any, []string) + fieldDiffCheck = func(t *testing.T, r, w any) (bool, any, any, []string) { + if r == nil && w != nil || r != nil && w == nil { + return false, r, w, nil + } + + var fields []string + if reflect.DeepEqual(r, w) { + return true, nil, nil, nil + } + switch rVal := r.(type) { + case map[string]any: + wVal := w.(map[string]any) + for key, val := range rVal { + fields = append(fields, key) + if equal, rDiff, wDiff, subfs := fieldDiffCheck(t, val, wVal[key]); !equal { + fields = append(fields, subfs...) + return false, rDiff, wDiff, fields + } + } + } + if reflect.DeepEqual(r, w) { + return true, nil, nil, nil + } + return false, r, w, nil + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + r Redis + w Redis + ) + if err := yaml.Unmarshal([]byte(tt.data), &r); err != nil { + t.Errorf("yaml.Unmarshal() raw error = %v", err) + } + if err := yaml.Unmarshal([]byte(tt.want), &w); err != nil { + t.Errorf("yaml.Unmarshal() wanted error = %v", err) + } + r.Default() + + if !reflect.DeepEqual(r.ObjectMeta, w.ObjectMeta) { + rData, _ := json.Marshal(r.ObjectMeta) + wData, _ := json.Marshal(w.ObjectMeta) + t.Errorf("Redis.Default().metadata= %s, <<<<<<<>>>>>>> want %s", rData, wData) + } + + rPauseAnnVal, rPauseOk := r.Spec.PodAnnotations[PauseAnnotationKey] + wPauseAnnVal, wPauseOk := w.Spec.PodAnnotations[PauseAnnotationKey] + if rPauseOk != wPauseOk { + t.Errorf("Redis.Default().spec.podAnnotations.%s = %v, <<<<<<<>>>>>>> want %v", PauseAnnotationKey, rPauseAnnVal, wPauseAnnVal) + } + delete(r.Spec.PodAnnotations, PauseAnnotationKey) + delete(w.Spec.PodAnnotations, PauseAnnotationKey) + + rJsonData, _ := json.Marshal(r.Spec) + wJsonData, _ := json.Marshal(w.Spec) + var ( + rData = map[string]any{} + wData = map[string]any{} + ) + if err := json.Unmarshal(rJsonData, &rData); err != nil { + t.Errorf("json.Unmarshal() raw error = %v", err) + } + if err := json.Unmarshal(wJsonData, &wData); err != nil { + t.Errorf("json.Unmarshal() wanted error = %v", err) + } + + keys := lo.Keys(rData) + for key := range wData { + if !slices.Contains(keys, key) { + keys = append(keys, key) + } + } + for _, key := range keys { + if !reflect.DeepEqual(rData[key], wData[key]) { + if equal, rDiff, wDiff, fields := fieldDiffCheck(t, rData[key], wData[key]); !equal { + t.Errorf("Redis.Default().spec.%s.%v = %v, <<<<<<<>>>>>>> want %v", key, strings.Join(fields, "."), rDiff, wDiff) + } else { + t.Errorf("Redis.Default().spec.%v = %v, <<<<<<<>>>>>>> want %v", key, rData[key], wData[key]) + } + } + } + }) + } +} + +func TestRedis_ValidateCreateCluster(t *testing.T) { + tests := []struct { + name string + redis *Redis + wantErr error + wantWarns admission.Warnings + }{ + { + name: "cluster nil replcas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: nil, + }, + }, + wantErr: fmt.Errorf("instance replicas not specified"), + wantWarns: nil, + }, + { + name: "cluster enabled nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "cluster matched nodeport count", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001,30002-30004,30005", + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "cluster not matched nodeport count", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30002-30004,30005", + }, + }, + }, + }, + wantErr: fmt.Errorf("expected 6 nodes, but got 5 ports in node port sequence"), + wantWarns: nil, + }, + { + name: "cluster with matched specified shards", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "cluster with not matched specified shards", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-10000"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("specified shard slots list length not not match shards count"), + wantWarns: nil, + }, + { + name: "cluster specified shards with invalid slots", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16433"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("failed to load shard slots: invalid slot 16384"), + wantWarns: nil, + }, + { + name: "cluster specified shards with invalid slots2", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "aaaaa"}, + {Slots: "5462-10922"}, + {Slots: "10923-16433"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("failed to load shard slots: invalid range slot aaaaa"), + wantWarns: nil, + }, + { + name: "cluster shards specified slots not fullfilled", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10921"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("specified shard slots not fullfilled"), + wantWarns: nil, + }, + { + name: "cluster shards specified slots duplicated", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5462"}, + {Slots: "5462-10923"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("specified shard slots duplicated"), + wantWarns: nil, + }, + { + name: "cluster enabled activeredis without serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + + { + name: "cluster enabled activeredis", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(1), + }, + }, + wantErr: nil, + wantWarns: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warns, err := tt.redis.ValidateCreate() + if tt.wantErr != nil { + if err == nil { + t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr) + } else if err.Error() != tt.wantErr.Error() { + t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr) + } + } else if err != nil { + t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateCreate() warns = %v, wantWarns %v", warns, tt.wantWarns) + } + }) + } +} + +func TestRedis_ValidateCreateFailover(t *testing.T) { + tests := []struct { + name string + redis *Redis + wantErr error + wantWarns admission.Warnings + }{ + { + name: "failover nil replcas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: nil, + }, + }, + wantErr: fmt.Errorf("instance replicas not specified"), + wantWarns: nil, + }, + { + name: "failover not enable nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{}, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 1, + }, + }, + }, + }, + wantErr: fmt.Errorf("sentinel replicas must be odd and greater >= 3"), + wantWarns: nil, + }, + { + name: "failover not enable nodeport, with invalid sentinel replicas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{}, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 4, + }, + }, + }, + }, + wantErr: fmt.Errorf("sentinel replicas must be odd and greater >= 3"), + wantWarns: nil, + }, + { + name: "failover enabled nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{}, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "failover matched nodeport count", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "31000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "failover nodeport not match--data node", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "31000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("expected 3 nodes, but got 2 ports in node port sequence"), + wantWarns: nil, + }, + { + name: "failover nodeport not match--sentinel node", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("expected 3 nodes, but got 2 ports in node port sequence"), + wantWarns: nil, + }, + { + name: "failover duplicate nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("port 30000 has assigned to spec.expose.dataStorageNodePortSequence"), + wantWarns: nil, + }, + { + name: "failover duplicate nodeport with accessPort", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + AccessPort: 31001, + NodePortSequence: "31000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("port 31001 has assigned to spec.sentinel.expose.accessPort"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis without serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(-1), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(16), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(1), + }, + }, + wantErr: nil, + wantWarns: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warns, err := tt.redis.ValidateCreate() + if tt.wantErr != nil { + if err == nil { + t.Errorf("ValidateCreateFailover() error = %v, wantErr %v", err, tt.wantErr) + } else if err.Error() != tt.wantErr.Error() { + t.Errorf("ValidateCreateFailover() error = %v, wantErr %v", err, tt.wantErr) + } + } else if err != nil { + t.Errorf("ValidateCreateFailover() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateCreateFailover() warns = %v, wantWarns %v", warns, tt.wantWarns) + } + }) + } +} + +func TestRedis_ValidateCreateStandalone(t *testing.T) { + tests := []struct { + name string + redis *Redis + wantErr error + wantWarns admission.Warnings + }{ + { + name: "standalone nil replcas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: nil, + }, + }, + wantErr: fmt.Errorf("spec.replicas.sentinel.master must be 1"), + wantWarns: nil, + }, + { + name: "standalone ok", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{}, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "standalone enabled nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "standalone enable nodeport with custom port", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000", + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "standalone enabled activeredis without serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "standalone enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(-1), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "standalone enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(16), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "standalone enabled activeredis", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(1), + }, + }, + wantErr: nil, + wantWarns: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warns, err := tt.redis.ValidateCreate() + if tt.wantErr != nil { + if err == nil { + t.Errorf("ValidateCreateStandalone() error = %v, wantErr %v", err, tt.wantErr) + } else if err.Error() != tt.wantErr.Error() { + t.Errorf("ValidateCreateStandalone() error = %v, wantErr %v", err, tt.wantErr) + } + } else if err != nil { + t.Errorf("ValidateCreateStandalone() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateCreateStandalone() warns = %v, wantWarns %v", warns, tt.wantWarns) + } + }) + } +} + +func TestRedis_ValidateUpdateCluster(t *testing.T) { + tests := []struct { + name string + redis *Redis + wantErr error + wantWarns admission.Warnings + }{ + { + name: "cluster nil replcas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: nil, + }, + }, + wantErr: fmt.Errorf("instance replicas not specified"), + wantWarns: nil, + }, + { + name: "cluster enabled nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "cluster matched nodeport count", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001,30002-30004,30005", + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "cluster not matched nodeport count", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30002-30004,30005", + }, + }, + }, + }, + wantErr: fmt.Errorf("expected 6 nodes, but got 5 ports in node port sequence"), + wantWarns: nil, + }, + { + name: "cluster with matched specified shards", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "cluster with not matched specified shards#not work for update", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-10000"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "cluster specified shards with invalid slots", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16433"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("failed to load shard slots: invalid slot 16384"), + wantWarns: nil, + }, + { + name: "cluster specified shards with invalid slots2", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "aaaaa"}, + {Slots: "5462-10922"}, + {Slots: "10923-16433"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("failed to load shard slots: invalid range slot aaaaa"), + wantWarns: nil, + }, + { + name: "cluster shards specified slots not fullfilled", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10921"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("specified shard slots not fullfilled"), + wantWarns: nil, + }, + { + name: "cluster shards specified slots duplicated", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5462"}, + {Slots: "5462-10923"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: fmt.Errorf("specified shard slots duplicated"), + wantWarns: nil, + }, + { + name: "cluster enabled activeredis without serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + + { + name: "cluster enabled activeredis", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisCluster, + Replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + Shards: []clusterv1.ClusterShardConfig{ + {Slots: "0-5461"}, + {Slots: "5462-10922"}, + {Slots: "10923-16383"}, + }, + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(1), + }, + }, + wantErr: nil, + wantWarns: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warns, err := tt.redis.ValidateUpdate(nil) + if tt.wantErr != nil { + if err == nil { + t.Errorf("ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr) + } else if err.Error() != tt.wantErr.Error() { + t.Errorf("ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr) + } + } else if err != nil { + t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateUpdate() warns = %v, wantWarns %v", warns, tt.wantWarns) + } + }) + } +} + +func TestRedis_ValidateUpdateFailover(t *testing.T) { + tests := []struct { + name string + redis *Redis + wantErr error + wantWarns admission.Warnings + }{ + { + name: "failover nil replcas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: nil, + }, + }, + wantErr: fmt.Errorf("instance replicas not specified"), + wantWarns: nil, + }, + { + name: "failover not enable nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{}, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 1, + }, + }, + }, + }, + wantErr: fmt.Errorf("sentinel replicas must be odd and greater >= 3"), + wantWarns: nil, + }, + { + name: "failover not enable nodeport, with invalid sentinel replicas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{}, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 4, + }, + }, + }, + }, + wantErr: fmt.Errorf("sentinel replicas must be odd and greater >= 3"), + wantWarns: nil, + }, + { + name: "failover enabled nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{}, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "failover matched nodeport count", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "31000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "failover nodeport not match--data node", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "31000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("expected 3 nodes, but got 2 ports in node port sequence"), + wantWarns: nil, + }, + { + name: "failover nodeport not match--sentinel node", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("expected 3 nodes, but got 2 ports in node port sequence"), + wantWarns: nil, + }, + { + name: "failover duplicate nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("port 30000 has assigned to spec.expose.dataStorageNodePortSequence"), + wantWarns: nil, + }, + { + name: "failover duplicate nodeport with accessPort", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000,30001-30002", + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + AccessPort: 31001, + NodePortSequence: "31000,31001-31002", + }, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("port 31001 has assigned to spec.sentinel.expose.accessPort"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis without serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(-1), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(16), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "failover enabled activeredis", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisSentinel, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + Sentinel: &redisfailoverv1.SentinelSettings{ + RedisSentinelSpec: redisfailoverv1.RedisSentinelSpec{ + Replicas: 3, + Expose: core.InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(1), + }, + }, + wantErr: nil, + wantWarns: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warns, err := tt.redis.ValidateUpdate(nil) + if tt.wantErr != nil { + if err == nil { + t.Errorf("ValidateUpdateFailover() error = %v, wantErr %v", err, tt.wantErr) + } else if err.Error() != tt.wantErr.Error() { + t.Errorf("ValidateUpdatekFailover() error = %v, wantErr %v", err, tt.wantErr) + } + } else if err != nil { + t.Errorf("ValidateUpdateFailover() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateUpdateFailover() warns = %v, wantWarns %v", warns, tt.wantWarns) + } + }) + } +} + +func TestRedis_ValidateUpdateStandalone(t *testing.T) { + tests := []struct { + name string + redis *Redis + wantErr error + wantWarns admission.Warnings + }{ + { + name: "standalone nil replcas", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: nil, + }, + }, + wantErr: fmt.Errorf("spec.replicas.sentinel.master must be 1"), + wantWarns: nil, + }, + { + name: "standalone ok", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{}, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "standalone enabled nodeport", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "standalone enable nodeport with custom port", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + NodePortSequence: "30000", + }, + }, + }, + }, + wantErr: nil, + wantWarns: nil, + }, + { + name: "standalone enabled activeredis without serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "standalone enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(-1), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "standalone enabled activeredis with invalid serviceID", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(16), + }, + }, + wantErr: fmt.Errorf("activeredis is enabled but serviceID is not valid"), + wantWarns: nil, + }, + { + name: "standalone enabled activeredis", + redis: &Redis{ + Spec: RedisSpec{ + Arch: core.RedisStandalone, + Replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + Expose: InstanceAccess{ + InstanceAccessBase: core.InstanceAccessBase{ + ServiceType: corev1.ServiceTypeNodePort, + }, + }, + EnableActiveRedis: true, + ServiceID: pointer.Int32(1), + }, + }, + wantErr: nil, + wantWarns: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warns, err := tt.redis.ValidateUpdate(nil) + if tt.wantErr != nil { + if err == nil { + t.Errorf("ValidateUpdateStandalone() error = %v, wantErr %v", err, tt.wantErr) + } else if err.Error() != tt.wantErr.Error() { + t.Errorf("ValidateCreateStandalone() error = %v, wantErr %v", err, tt.wantErr) + } + } else if err != nil { + t.Errorf("ValidateUpdateStandalone() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateUpdateStandalone() warns = %v, wantWarns %v", warns, tt.wantWarns) + } + }) + } +} + +func TestRedis_ValidateDelete(t *testing.T) { + redis := &Redis{} + _, err := redis.ValidateDelete() + if err != nil { + t.Errorf("ValidateDelete() error = %v, wantErr %v", err, false) + } +} + +func Test_getNodeCountByArch(t *testing.T) { + type args struct { + arch core.Arch + replicas *RedisReplicas + } + tests := []struct { + name string + args args + want int + }{ + { + name: "cluster 3-0", + args: args{ + arch: core.RedisCluster, + replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + }, + }, + }, + want: 3, + }, + { + name: "cluster 3-1", + args: args{ + arch: core.RedisCluster, + replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(1), + }, + }, + }, + want: 6, + }, + { + name: "cluster 3-2", + args: args{ + arch: core.RedisCluster, + replicas: &RedisReplicas{ + Cluster: &ClusterReplicas{ + Shard: pointer.Int32(3), + Slave: pointer.Int32(2), + }, + }, + }, + want: 9, + }, + { + name: "sentinel 1", + args: args{ + arch: core.RedisSentinel, + replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + }, + want: 1, + }, + { + name: "sentinel 2", + args: args{ + arch: core.RedisSentinel, + replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(1), + }, + }, + }, + want: 2, + }, + { + name: "sentinel 3", + args: args{ + arch: core.RedisSentinel, + replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + Slave: pointer.Int32(2), + }, + }, + }, + want: 3, + }, + { + name: "standalone 0", + args: args{ + arch: core.RedisStandalone, + replicas: &RedisReplicas{}, + }, + want: 1, + }, + { + name: "standalone 1", + args: args{ + arch: core.RedisStandalone, + replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(1), + }, + }, + }, + want: 1, + }, + { + name: "standalone any", + args: args{ + arch: core.RedisStandalone, + replicas: &RedisReplicas{ + Sentinel: &SentinelReplicas{ + Master: pointer.Int32(10), + Slave: pointer.Int32(3), + }, + }, + }, + want: 1, + }, + { + name: "empty", + args: args{}, + want: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getNodeCountByArch(tt.args.arch, tt.args.replicas); got != tt.want { + t.Errorf("getNodeCountByArch() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/api/middleware/v1/validation/validation.go b/api/middleware/v1/validation/validation.go new file mode 100644 index 0000000..ef5ae0c --- /dev/null +++ b/api/middleware/v1/validation/validation.go @@ -0,0 +1,137 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validation + +import ( + "context" + "fmt" + + security "github.com/alauda/redis-operator/pkg/security/password" + + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var ( + MinMemoryLimits = resource.NewQuantity(1<<25, resource.BinarySI) + WarningMinMemoryLimits = resource.NewQuantity(1<<27, resource.BinarySI) + WarningMaxMemoryLimits = resource.NewQuantity(1<<35, resource.BinarySI) +) + +const ( + MinMaxMemoryPercentage = 0.8 +) + +// ValidateClusterScaling validates the scaling of a cluster +func ValidateClusterScalingResource(shards int32, resReq *corev1.ResourceRequirements, datasets []int64, warns *admission.Warnings) (err error) { + if resReq == nil || resReq.Limits.Memory().Value() == 0 { + return nil + } + + memoryLimit := resReq.Limits.Memory() + if memoryLimit.Cmp(*MinMemoryLimits) < 0 { + return fmt.Errorf("memory limit is too low, must be at least %s", MinMemoryLimits.String()) + } + if memoryLimit.Cmp(*WarningMinMemoryLimits) < 0 { + *warns = append(*warns, fmt.Sprintf("memory limit it's recommended to be at least %s", WarningMinMemoryLimits.String())) + } + if memoryLimit.Cmp(*WarningMaxMemoryLimits) > 0 { + *warns = append(*warns, fmt.Sprintf("memory limit it's recommended to be at most %s", WarningMaxMemoryLimits.String())) + } + + if len(datasets) >= 3 { + maxdatasets := float64(lo.Max(datasets)) + memoryLimitVal := float64(memoryLimit.Value()) + + if int(shards) >= len(datasets) { + if wanted := maxdatasets / MinMaxMemoryPercentage; memoryLimitVal < wanted { + res := resource.NewQuantity(int64(wanted), resource.BinarySI) + return fmt.Errorf("memory limit may can't serve current dataset, should be at least %s", res.String()) + } + } else { + // NOTE: every shard should be able to hold all the deleting shards + mergedDataSize := float64(lo.Sum(datasets[shards:])) + maxdatasets = float64(lo.Max(datasets[0:shards])) + mergedDataSize + if wanted := maxdatasets / MinMaxMemoryPercentage; memoryLimitVal < wanted { + res := resource.NewQuantity(int64(wanted), resource.BinarySI) + return fmt.Errorf("memory limit may can't serve current dataset, shoud be at least %s", res.String()) + } + } + } + return +} + +// ValidateReplicationScaling validates the scaling of a single replication +func ValidateReplicationScalingResource(resReq *corev1.ResourceRequirements, datasets int64, warns *admission.Warnings) (err error) { + if resReq == nil || resReq.Limits.Memory().Value() == 0 { + return nil + } + + memoryLimit := resReq.Limits.Memory() + if memoryLimit.Cmp(*MinMemoryLimits) < 0 { + return fmt.Errorf("memory limit is too low, must be at least %s", MinMemoryLimits.String()) + } + if memoryLimit.Cmp(*WarningMinMemoryLimits) < 0 { + *warns = append(*warns, fmt.Sprintf("memory limit it's recommended to be at least %s", WarningMinMemoryLimits.String())) + } + if memoryLimit.Cmp(*WarningMaxMemoryLimits) > 0 { + *warns = append(*warns, fmt.Sprintf("memory limit it's recommended to be at most %s", WarningMaxMemoryLimits.String())) + } + + if datasets > 0 { + maxdatasets := float64(datasets) + memoryLimitVal := float64(memoryLimit.Value()) + if wanted := maxdatasets / MinMaxMemoryPercentage; memoryLimitVal < wanted { + res := resource.NewQuantity(int64(wanted), resource.BinarySI) + return fmt.Errorf("memory limit may can't serve current dataset, should be at least %s", res.String()) + } + } + return +} + +func ValidateActiveRedisService(f bool, serviceID *int32, warns *admission.Warnings) (err error) { + if !f { + return nil + } + + if serviceID == nil || *serviceID < 0 || *serviceID > 15 { + return fmt.Errorf("activeredis is enabled but serviceID is not valid") + } + return +} + +func ValidatePasswordSecret(namespace, secretName string, mgrClient client.Client, warns *admission.Warnings) error { + if mgrClient == nil { + return nil + } + if secretName != "" { + secret := &v1.Secret{} + if err := mgrClient.Get(context.Background(), types.NamespacedName{ + Namespace: namespace, + Name: secretName, + }, secret); err != nil { + return err + } + return security.PasswordValidate(string(secret.Data["password"]), 8, 32) + } + return nil +} diff --git a/api/middleware/v1/validation/validation_test.go b/api/middleware/v1/validation/validation_test.go new file mode 100644 index 0000000..1fa74fd --- /dev/null +++ b/api/middleware/v1/validation/validation_test.go @@ -0,0 +1,437 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validation + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestValidateClusterScalingResource(t *testing.T) { + dss := int64(1) << 30 + memReq := int64(float64(dss)/float64(MinMaxMemoryPercentage)) + 1 + + type args struct { + shards int32 + resource *corev1.ResourceRequirements + datasize []int64 + } + tests := []struct { + name string + args args + wantErr bool + wantWarns admission.Warnings + }{ + { + name: "just match the maxmemory limit", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(memReq, resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss}, + }, + wantErr: false, + }, + { + name: "just not match the maxmemory limit", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(memReq-2, resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss}, + }, + wantErr: true, + }, + { + name: "nil resource check", + args: args{ + shards: 3, + resource: nil, + }, + wantErr: false, + }, + { + name: "nil resource check with data", + args: args{ + shards: 3, + datasize: []int64{dss, dss, dss}, + }, + wantErr: false, + }, + { + name: "empty resource check", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{}, + }, + wantErr: false, + }, + { + name: "empty resource check with data", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{}, + datasize: []int64{dss, dss, dss}, + }, + wantErr: false, + }, + { + name: "min memory limit check", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(1<<24, resource.BinarySI), + }, + }, + }, + wantErr: true, + }, + { + name: "min memory limit check with warning", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(1<<25, resource.BinarySI), + }, + }, + }, + wantErr: false, + wantWarns: admission.Warnings{ + "memory limit it's recommended to be at least 128Mi", + }, + }, + { + name: "max memory limit check with warning", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(1<<36, resource.BinarySI), + }, + }, + }, + wantErr: false, + wantWarns: admission.Warnings{"memory limit it's recommended to be at most 32Gi"}, + }, + { + name: "3=>6 without change memory limit", + args: args{ + shards: 6, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(memReq, resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss}, + }, + wantErr: false, + }, + { + name: "3=>6 without update the memory limit", + args: args{ + shards: 6, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(memReq, resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss}, + }, + wantErr: false, + }, + { + name: "3=>6 with halve the memory limit", + args: args{ + shards: 6, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(1+memReq/2, resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss}, + }, + wantErr: true, + }, + { + name: "4=>3 with not scaling memory", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(memReq, resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss, dss}, + }, + wantErr: true, + }, + { + name: "4=>3 with just match memory", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(int64(float64(dss+dss)/MinMaxMemoryPercentage), resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss, dss}, + }, + wantErr: false, + }, + { + name: "4=>3 with only the deleting shards have data", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(int64(float64(dss)/MinMaxMemoryPercentage), resource.BinarySI), + }, + }, + datasize: []int64{0, 0, 0, dss}, + }, + wantErr: false, + }, + { + name: "4=>3 with the deleting shards is empty", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(int64(float64(dss)/MinMaxMemoryPercentage), resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss, 0}, + }, + wantErr: false, + }, + { + name: "6=>3 deleting 3 shards", + args: args{ + shards: 3, + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(int64(float64(dss*4)/MinMaxMemoryPercentage), resource.BinarySI), + }, + }, + datasize: []int64{dss, dss, dss, dss, dss, dss}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var warns admission.Warnings + if err := ValidateClusterScalingResource(tt.args.shards, tt.args.resource, tt.args.datasize, &warns); (err != nil) != tt.wantErr { + t.Errorf("ValidateClusterScalingResource() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateClusterScalingResource() warns = %v, want %v", warns, tt.wantWarns) + } + }) + } +} + +func TestValidateReplicationScalingResource(t *testing.T) { + dss := int64(1) << 30 + memReq := int64(float64(dss)/float64(MinMaxMemoryPercentage)) + 1 + + type args struct { + resource *corev1.ResourceRequirements + datasize int64 + } + tests := []struct { + name string + args args + wantErr bool + wantWarns admission.Warnings + }{ + { + name: "just match the maxmemory limit", + args: args{ + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(memReq, resource.BinarySI), + }, + }, + datasize: dss, + }, + wantErr: false, + }, + { + name: "just not match the maxmemory limit", + args: args{ + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(memReq-2, resource.BinarySI), + }, + }, + datasize: dss, + }, + wantErr: true, + }, + { + name: "nil resource check", + args: args{ + resource: nil, + }, + wantErr: false, + }, + { + name: "nil resource check with data", + args: args{ + datasize: dss, + }, + wantErr: false, + }, + { + name: "empty resource check", + args: args{ + resource: &corev1.ResourceRequirements{}, + }, + wantErr: false, + }, + { + name: "empty resource check with data", + args: args{ + resource: &corev1.ResourceRequirements{}, + datasize: dss, + }, + wantErr: false, + }, + { + name: "min memory limit check", + args: args{ + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(1<<24, resource.BinarySI), + }, + }, + }, + wantErr: true, + }, + { + name: "min memory limit check with warning", + args: args{ + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(1<<25, resource.BinarySI), + }, + }, + }, + wantErr: false, + wantWarns: admission.Warnings{ + "memory limit it's recommended to be at least 128Mi", + }, + }, + { + name: "max memory limit check with warning", + args: args{ + resource: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: *resource.NewQuantity(1<<36, resource.BinarySI), + }, + }, + }, + wantErr: false, + wantWarns: admission.Warnings{"memory limit it's recommended to be at most 32Gi"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var warns admission.Warnings + if err := ValidateReplicationScalingResource(tt.args.resource, tt.args.datasize, &warns); (err != nil) != tt.wantErr { + t.Errorf("ValidateReplicationScalingResource() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(warns, tt.wantWarns) { + t.Errorf("ValidateClusterScalingResource() warns = %v, want %v", warns, tt.wantWarns) + } + }) + } +} + +func TestValidateActiveRedisService(t *testing.T) { + type args struct { + f bool + serviceID *int32 + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "not enabled", + args: args{ + f: false, + }, + wantErr: false, + }, + { + name: "enabled but serviceID=nil", + args: args{ + f: true, + serviceID: nil, + }, + wantErr: true, + }, + { + name: "enabled serviceID=1", + args: args{ + f: true, + serviceID: pointer.Int32(1), + }, + wantErr: false, + }, + { + name: "enabled serviceID=-1", + args: args{ + f: true, + serviceID: pointer.Int32(-1), + }, + wantErr: true, + }, + { + name: "enabled serviceID=16", + args: args{ + f: true, + serviceID: pointer.Int32(16), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var warns admission.Warnings + if err := ValidateActiveRedisService(tt.args.f, tt.args.serviceID, &warns); (err != nil) != tt.wantErr { + t.Errorf("ValidateActiveRedisService() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/api/middleware/v1/zz_generated.deepcopy.go b/api/middleware/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000..09c3b29 --- /dev/null +++ b/api/middleware/v1/zz_generated.deepcopy.go @@ -0,0 +1,532 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterReplicas) DeepCopyInto(out *ClusterReplicas) { + *out = *in + if in.Shard != nil { + in, out := &in.Shard, &out.Shard + *out = new(int32) + **out = **in + } + if in.Slave != nil { + in, out := &in.Slave, &out.Slave + *out = new(int32) + **out = **in + } + if in.Shards != nil { + in, out := &in.Shards, &out.Shards + *out = make([]v1alpha1.ClusterShardConfig, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterReplicas. +func (in *ClusterReplicas) DeepCopy() *ClusterReplicas { + if in == nil { + return nil + } + out := new(ClusterReplicas) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EmbeddedObjectMeta) DeepCopyInto(out *EmbeddedObjectMeta) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddedObjectMeta. +func (in *EmbeddedObjectMeta) DeepCopy() *EmbeddedObjectMeta { + if in == nil { + return nil + } + out := new(EmbeddedObjectMeta) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceAccess) DeepCopyInto(out *InstanceAccess) { + *out = *in + in.InstanceAccessBase.DeepCopyInto(&out.InstanceAccessBase) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceAccess. +func (in *InstanceAccess) DeepCopy() *InstanceAccess { + if in == nil { + return nil + } + out := new(InstanceAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Redis) DeepCopyInto(out *Redis) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Redis. +func (in *Redis) DeepCopy() *Redis { + if in == nil { + return nil + } + out := new(Redis) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Redis) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisList) DeepCopyInto(out *RedisList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Redis, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisList. +func (in *RedisList) DeepCopy() *RedisList { + if in == nil { + return nil + } + out := new(RedisList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisPatchSpec) DeepCopyInto(out *RedisPatchSpec) { + *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]*Service, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Service) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisPatchSpec. +func (in *RedisPatchSpec) DeepCopy() *RedisPatchSpec { + if in == nil { + return nil + } + out := new(RedisPatchSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisPersistent) DeepCopyInto(out *RedisPersistent) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisPersistent. +func (in *RedisPersistent) DeepCopy() *RedisPersistent { + if in == nil { + return nil + } + out := new(RedisPersistent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisProxy) DeepCopyInto(out *RedisProxy) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisProxy. +func (in *RedisProxy) DeepCopy() *RedisProxy { + if in == nil { + return nil + } + out := new(RedisProxy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisReplicas) DeepCopyInto(out *RedisReplicas) { + *out = *in + if in.Cluster != nil { + in, out := &in.Cluster, &out.Cluster + *out = new(ClusterReplicas) + (*in).DeepCopyInto(*out) + } + if in.Sentinel != nil { + in, out := &in.Sentinel, &out.Sentinel + *out = new(SentinelReplicas) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisReplicas. +func (in *RedisReplicas) DeepCopy() *RedisReplicas { + if in == nil { + return nil + } + out := new(RedisReplicas) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSpec) DeepCopyInto(out *RedisSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Persistent != nil { + in, out := &in.Persistent, &out.Persistent + *out = new(RedisPersistent) + **out = **in + } + if in.PersistentSize != nil { + in, out := &in.PersistentSize, &out.PersistentSize + x := (*in).DeepCopy() + *out = &x + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(RedisReplicas) + (*in).DeepCopyInto(*out) + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(corev1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.CustomConfig != nil { + in, out := &in.CustomConfig, &out.CustomConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.SentinelCustomConfig != nil { + in, out := &in.SentinelCustomConfig, &out.SentinelCustomConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.RedisProxy != nil { + in, out := &in.RedisProxy, &out.RedisProxy + *out = new(RedisProxy) + (*in).DeepCopyInto(*out) + } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Expose.DeepCopyInto(&out.Expose) + if in.Exporter != nil { + in, out := &in.Exporter, &out.Exporter + *out = new(databasesv1.RedisExporter) + (*in).DeepCopyInto(*out) + } + if in.Sentinel != nil { + in, out := &in.Sentinel, &out.Sentinel + *out = new(databasesv1.SentinelSettings) + (*in).DeepCopyInto(*out) + } + in.Backup.DeepCopyInto(&out.Backup) + out.Restore = in.Restore + if in.ServiceID != nil { + in, out := &in.ServiceID, &out.ServiceID + *out = new(int32) + **out = **in + } + if in.UpgradeOption != nil { + in, out := &in.UpgradeOption, &out.UpgradeOption + *out = new(UpgradeOption) + (*in).DeepCopyInto(*out) + } + if in.Patches != nil { + in, out := &in.Patches, &out.Patches + *out = new(RedisPatchSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSpec. +func (in *RedisSpec) DeepCopy() *RedisSpec { + if in == nil { + return nil + } + out := new(RedisSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisStatus) DeepCopyInto(out *RedisStatus) { + *out = *in + if in.MatchLabels != nil { + in, out := &in.MatchLabels, &out.MatchLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ProxyMatchLabels != nil { + in, out := &in.ProxyMatchLabels, &out.ProxyMatchLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ClusterNodes != nil { + in, out := &in.ClusterNodes, &out.ClusterNodes + *out = make([]core.RedisNode, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.UpgradeStatus = in.UpgradeStatus + if in.DetailedStatusRef != nil { + in, out := &in.DetailedStatusRef, &out.DetailedStatusRef + *out = new(corev1.ObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisStatus. +func (in *RedisStatus) DeepCopy() *RedisStatus { + if in == nil { + return nil + } + out := new(RedisStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SentinelReplicas) DeepCopyInto(out *SentinelReplicas) { + *out = *in + if in.Master != nil { + in, out := &in.Master, &out.Master + *out = new(int32) + **out = **in + } + if in.Slave != nil { + in, out := &in.Slave, &out.Slave + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SentinelReplicas. +func (in *SentinelReplicas) DeepCopy() *SentinelReplicas { + if in == nil { + return nil + } + out := new(SentinelReplicas) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SentinelSettings) DeepCopyInto(out *SentinelSettings) { + *out = *in + in.RedisSentinelSpec.DeepCopyInto(&out.RedisSentinelSpec) + if in.ExternalSentinel != nil { + in, out := &in.ExternalSentinel, &out.ExternalSentinel + *out = new(databasesv1.SentinelReference) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SentinelSettings. +func (in *SentinelSettings) DeepCopy() *SentinelSettings { + if in == nil { + return nil + } + out := new(SentinelSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Service) DeepCopyInto(out *Service) { + *out = *in + if in.EmbeddedObjectMeta != nil { + in, out := &in.EmbeddedObjectMeta, &out.EmbeddedObjectMeta + *out = new(EmbeddedObjectMeta) + (*in).DeepCopyInto(*out) + } + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Service. +func (in *Service) DeepCopy() *Service { + if in == nil { + return nil + } + out := new(Service) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpgradeOption) DeepCopyInto(out *UpgradeOption) { + *out = *in + if in.AutoUpgrade != nil { + in, out := &in.AutoUpgrade, &out.AutoUpgrade + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeOption. +func (in *UpgradeOption) DeepCopy() *UpgradeOption { + if in == nil { + return nil + } + out := new(UpgradeOption) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpgradeStatus) DeepCopyInto(out *UpgradeStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeStatus. +func (in *UpgradeStatus) DeepCopy() *UpgradeStatus { + if in == nil { + return nil + } + out := new(UpgradeStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/redis.kun/v1alpha1/distributedrediscluster_backup_types.go b/api/redis.kun/v1alpha1/distributedrediscluster_backup_types.go deleted file mode 100644 index cbedefd..0000000 --- a/api/redis.kun/v1alpha1/distributedrediscluster_backup_types.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -type BackupSourceSpec struct { - Namespace string `json:"namespace"` - Name string `json:"name"` - // Arguments to the restore job - Args []string `json:"args,omitempty"` -} - -// RedisStorage defines the structure used to store the Redis Data -type RedisStorage struct { - Size resource.Quantity `json:"size"` - Type StorageType `json:"type,omitempty"` - Class string `json:"class"` - DeleteClaim bool `json:"deleteClaim,omitempty"` -} - -// RedisRestore defines the structure used to restore the Redis Data -type RedisRestore struct { - Image string `json:"image,omitempty"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` - BackupName string `json:"backupName,omitempty"` -} - -// RedisBackup defines the structure used to backup the Redis Data -type RedisBackup struct { - Image string `json:"image,omitempty"` - Schedule []Schedule `json:"schedule,omitempty"` -} - -type Schedule struct { - Name string `json:"name,omitempty"` - Schedule string `json:"schedule"` - Keep int32 `json:"keep"` - KeepAfterDeletion bool `json:"keepAfterDeletion,omitempty"` - Storage RedisBackupStorage `json:"storage"` - Target databasesv1.RedisBackupTarget `json:"target,omitempty"` -} - -type RedisBackupStorage struct { - StorageClassName string `json:"storageClassName,omitempty"` - Size resource.Quantity `json:"size,omitempty"` -} diff --git a/api/redis.kun/v1alpha1/distributedrediscluster_dc_types.go b/api/redis.kun/v1alpha1/distributedrediscluster_dc_types.go deleted file mode 100644 index 0b3d87d..0000000 --- a/api/redis.kun/v1alpha1/distributedrediscluster_dc_types.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import "github.com/alauda/redis-operator/pkg/types/redis" - -// DC settings -type DcOption struct { - InitRole redis.RedisRole `json:"initRole,omitempty"` - InitADDRESS []RedisAddress `json:"initAddress,omitempty"` - HealPolicy HealPolicy `json:"healPolicy,omitempty"` - CheckerPolicy CheckerPolicy `json:"checkerPolicy,omitempty"` -} - -type CheckerPolicy struct { - PfailNodeCnt int `json:"pfailNodeCnt,omitempty"` - Retry int `json:"retry,omitempty"` - Enable bool `json:"enable,omitempty"` -} - -type RedisAddress struct { - IP string `json:"Ip,omitempty"` - Port string `json:"Port,omitempty"` - BusPort string `json:"busPort,omitempty"` -} - -type HealPolicy string - -const ( - None HealPolicy = "none" - Normal HealPolicy = "normal" - Force HealPolicy = "force" - TakeOver HealPolicy = "takeover" - TakeOverAndForget HealPolicy = "takeoverAndForget" -) diff --git a/api/redis/v1/redisbackup_types.go b/api/redis/v1/redisbackup_types.go deleted file mode 100644 index aff7af4..0000000 --- a/api/redis/v1/redisbackup_types.go +++ /dev/null @@ -1,220 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1 - -import ( - "fmt" - "path" - "time" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/config" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const RedisBackupKind = "RedisKind" - -// RedisBackupSource -type RedisBackupSource struct { - // RedisFailoverName redisfailover name - RedisFailoverName string `json:"redisFailoverName,omitempty"` - // RedisName redis instance name - RedisName string `json:"redisName,omitempty"` - // StorageClassName - StorageClassName string `json:"storageClassName,omitempty"` - // SourceType redis cluster type - SourceType ClusterType `json:"sourceType,omitempty"` - // Endpoint redis endpoint - Endpoint []IpPort `json:"endPoint,omitempty"` - // PasswordSecret - PasswordSecret string `json:"passwordSecret,omitempty"` - // SSLSecretName redis ssl secret name - SSLSecretName string `json:"SSLSecretName,omitempty"` -} - -// RedisBackupSpec defines the desired state of RedisBackup -type RedisBackupSpec struct { - // Source - Source RedisBackupSource `json:"source,omitempty"` - // Storage - Storage resource.Quantity `json:"storage,omitempty"` - // Image - Image string `json:"image,omitempty"` - // Target backup target - Target databasesv1.RedisBackupTarget `json:"target,omitempty"` - // Resources resource requirements for the job - Resources corev1.ResourceRequirements `json:"resources,omitempty"` - // BackoffLimit backoff limit for the job - BackoffLimit *int32 `json:"backoffLimit,omitempty" protobuf:"varint,7,opt,name=backoffLimit"` - // ActiveDeadlineSeconds active deadline seconds for the job - ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty" protobuf:"varint,3,opt,name=activeDeadlineSeconds"` - // NodeSelector node selector for the job - NodeSelector map[string]string `json:"nodeSelector,omitempty" protobuf:"bytes,7,rep,name=nodeSelector"` - // SecurityContext security context for the job - SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty" protobuf:"bytes,14,opt,name=securityContext"` - // ImagePullSecrets image pull secrets for the job - ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,15,rep,name=imagePullSecrets"` - // Tolerations tolerations for the job - Tolerations []corev1.Toleration `json:"tolerations,omitempty" protobuf:"bytes,22,opt,name=tolerations"` - // Affinity affinity for the job - Affinity *corev1.Affinity `json:"affinity,omitempty" protobuf:"bytes,18,opt,name=affinity"` - // PriorityClassName priority class name for the job - PriorityClassName string `json:"priorityClassName,omitempty" protobuf:"bytes,24,opt,name=priorityClassName"` - // RestoreCreateAt restore create at - RestoreCreateAt *metav1.Time `json:"restoreCreateAt,omitempty"` -} - -type RedisBackupTarget struct { - // S3Option - S3Option S3Option `json:"s3Option,omitempty"` - // GRPCOption grpc option - GRPCOption GRPCOption `json:"grpcOption,omitempty"` -} - -// GRPCOption TODO -type GRPCOption struct { - GRPC_PASSWORD_SECRET string `json:"secretName,omitempty" ` -} - -// S3Option -type S3Option struct { - // S3Secret - S3Secret string `json:"s3Secret,omitempty"` - // Bucket - Bucket string `json:"bucket,omitempty"` - // Dir - Dir string `json:"dir,omitempty"` -} - -// IpPort -type IpPort struct { - Address string `json:"address,omitempty"` - Port int64 `json:"port,omitempty"` - MasterName string `json:"masterName,omitempty"` -} - -type ClusterType string - -const ( - Cluster ClusterType = "cluster" - Sentinel ClusterType = "sentinel" - Standalone ClusterType = "standalone" -) - -// RedisBackupStatus defines the observed state of RedisBackup -type RedisBackupStatus struct { - // JobName - JobName string `json:"jobName,omitempty"` - // StartTime - StartTime *metav1.Time `json:"startTime,omitempty"` - // CompletionTime - CompletionTime *metav1.Time `json:"completionTime,omitempty"` - // Destination where store backup data in - // +optional - Destination string `json:"destination,omitempty"` - // Condition - Condition RedisBackupCondition `json:"condition,omitempty"` - // LastCheckTime - LastCheckTime *metav1.Time `json:"lastCheckTime,omitempty"` - // show message when backup fail - // +optional - Message string `json:"message,omitempty"` -} - -// RedisBackupCondition -type RedisBackupCondition string - -// These are valid conditions of a redis backup. -const ( - // RedisBackupRunning means the job running its execution. - RedisBackupRunning RedisBackupCondition = "Running" - // RedisBackupComplete means the job has completed its execution. - RedisBackupComplete RedisBackupCondition = "Complete" - // RedisBackupFailed means the job has failed its execution. - RedisBackupFailed RedisBackupCondition = "Failed" - // "RedisDeleteFailed" means that the deletion of backup-related resources has failed. - RedisDeleteFailed RedisBackupCondition = "DeleteFailed" -) - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// RedisBackup is the Schema for the redisbackups API -type RedisBackup struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec RedisBackupSpec `json:"spec,omitempty"` - Status RedisBackupStatus `json:"status,omitempty"` -} - -// Validate set the values by default if not defined and checks if the values given are valid -func (r *RedisBackup) Validate() error { - if r.Spec.Source.RedisFailoverName == "" && r.Spec.Source.RedisName == "" { - return fmt.Errorf("RedisFailoverName is not valid") - } - if r.Spec.Storage.IsZero() { - return fmt.Errorf("backup storage can't be empty") - } - - if r.Spec.Target.S3Option.S3Secret != "" { - r.Spec.Source.Endpoint = []IpPort{{Address: fmt.Sprintf("rfs-%s", r.Spec.Source.RedisName), - Port: 26379, - MasterName: "mymaster", - }} - if r.Spec.Target.S3Option.Dir == "" { - currentTime := time.Now().UTC() - timeString := currentTime.Format("2006-01-02T15:04:05Z") - r.Spec.Target.S3Option.Dir = path.Join("data", "backup", "redis-sentinel", "manual", timeString) - } - if r.Spec.Image == "" { - r.Spec.Image = config.GetDefaultBackupImage() - } - if r.Spec.BackoffLimit == nil { - r.Spec.BackoffLimit = new(int32) - *r.Spec.BackoffLimit = 1 - } - } - if r.Spec.Resources.Limits.Cpu().IsZero() && r.Spec.Resources.Limits.Memory().IsZero() { - r.Spec.Resources = corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("500Mi"), - corev1.ResourceCPU: resource.MustParse("500m"), - }, - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("500m"), - }, - } - } - return nil -} - -//+kubebuilder:object:root=true - -// RedisBackupList contains a list of RedisBackup -type RedisBackupList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []RedisBackup `json:"items"` -} - -func init() { - SchemeBuilder.Register(&RedisBackup{}, &RedisBackupList{}) -} diff --git a/api/redis/v1/redisclusterbackup_types.go b/api/redis/v1/redisclusterbackup_types.go deleted file mode 100644 index 647ff93..0000000 --- a/api/redis/v1/redisclusterbackup_types.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1 - -import ( - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// RedisClusterBackupSpec defines the desired state of RedisClusterBackup -type RedisClusterBackupSpec struct { - // Source backup source - Source ClusterRedisBackupSource `json:"source,omitempty"` - // Storage backup storage - Storage resource.Quantity `json:"storage,omitempty"` - // Image backup image - Image string `json:"image,omitempty"` - // Target backup target - Target RedisBackupTarget `json:"target,omitempty"` - // BackoffLimit backoff limit - BackoffLimit *int32 `json:"backoffLimit,omitempty" protobuf:"varint,7,opt,name=backoffLimit"` - // ActiveDeadlineSeconds active deadline seconds - ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty" protobuf:"varint,3,opt,name=activeDeadlineSeconds"` - // SecurityContext security context - SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty" protobuf:"bytes,14,opt,name=securityContext"` - // ImagePullSecrets image pull secrets - ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,15,rep,name=imagePullSecrets"` - // Tolerations tolerations - Tolerations []corev1.Toleration `json:"tolerations,omitempty" protobuf:"bytes,22,opt,name=tolerations"` - // Affinity affinity - Affinity *corev1.Affinity `json:"affinity,omitempty" protobuf:"bytes,18,opt,name=affinity"` - // PriorityClassName priority class name - PriorityClassName string `json:"priorityClassName,omitempty" protobuf:"bytes,24,opt,name=priorityClassName"` - // NodeSelector node selector - NodeSelector map[string]string `json:"nodeSelector,omitempty" protobuf:"bytes,7,rep,name=nodeSelector"` - // Resources backup pod resource config - Resources *corev1.ResourceRequirements `json:"resources,omitempty"` -} - -// RedisClusterBackupStatus defines the observed state of RedisClusterBackup -type RedisClusterBackupStatus struct { - // JobName job name run this backup - JobName string `json:"jobName,omitempty"` - // StartTime start time - StartTime *metav1.Time `json:"startTime,omitempty"` - // CompletionTime completion time - CompletionTime *metav1.Time `json:"completionTime,omitempty"` - // optional - // where store backup data in - Destination string `json:"destination,omitempty"` - // Condition backup condition - Condition RedisBackupCondition `json:"condition,omitempty"` - // LastCheckTime last check time - LastCheckTime *metav1.Time `json:"lastCheckTime,omitempty"` - // show message when backup fail - Message string `json:"message,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// RedisClusterBackup is the Schema for the redisclusterbackups API -type RedisClusterBackup struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec RedisClusterBackupSpec `json:"spec,omitempty"` - Status RedisClusterBackupStatus `json:"status,omitempty"` -} - -// ClusterRedisBackupSource -type ClusterRedisBackupSource struct { - // RedisClusterName redis cluster name - RedisClusterName string `json:"redisClusterName,omitempty"` - // StorageClassName storage class name - StorageClassName string `json:"storageClassName,omitempty"` - // Endpoint redis cluster endpoint - Endpoint []IpPort `json:"endPoint,omitempty"` - // PasswordSecret password secret - PasswordSecret string `json:"passwordSecret,omitempty"` - // SSLSecretName ssl secret name - SSLSecretName string `json:"SSLSecretName,omitempty"` -} - -// +kubebuilder:object:root=true - -// RedisClusterBackupList contains a list of RedisBackup -type RedisClusterBackupList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []RedisClusterBackup `json:"items"` -} - -func init() { - SchemeBuilder.Register(&RedisClusterBackup{}, &RedisClusterBackupList{}) -} diff --git a/api/redis/v1/redisuser_webhook.go b/api/redis/v1/redisuser_webhook.go deleted file mode 100644 index 5858077..0000000 --- a/api/redis/v1/redisuser_webhook.go +++ /dev/null @@ -1,239 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1 - -import ( - "context" - "fmt" - "strings" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - clusterv1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/util" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -const RedisUserFinalizer = "redisusers.redis.middleware.alauda.io/finalizer" - -var redislog = log.Log.WithName("redisuser-webhook") -var dyClient client.Client - -func (r *RedisUser) SetupWebhookWithManager(mgr ctrl.Manager) error { - dyClient = mgr.GetClient() - return ctrl.NewWebhookManagedBy(mgr). - For(r). - Complete() -} - -//+kubebuilder:webhook:verbs=create;update,path=/mutate-redis-middleware-alauda-io-v1-redisuser,mutating=true,failurePolicy=fail,groups=redis.middleware.alauda.io,resources=redisusers,versions=v1,name=mredisuser.kb.io,sideEffects=none,admissionReviewVersions=v1beta1 -//+kubebuilder:webhook:verbs=create;update;delete,path=/validate-redis-middleware-alauda-io-v1-redisuser,mutating=false,failurePolicy=fail,groups=redis.middleware.alauda.io,resources=redisusers,versions=v1,name=vredisuser.kb.io,sideEffects=none,admissionReviewVersions=v1beta1 - -var _ webhook.Validator = &RedisUser{} -var _ webhook.Defaulter = &RedisUser{} - -func (r *RedisUser) Default() { - redislog.V(3).Info("default", "redisUser.name", r.Name) - if r.Labels == nil { - r.Labels = make(map[string]string) - } - r.Labels["managed-by"] = "redis-operator" - r.Labels["middleware.instance/name"] = r.Spec.RedisName - - if r.GetDeletionTimestamp() != nil { - return - } - if r.Spec.AclRules != "" { - // 转为小写 - r.Spec.AclRules = strings.ToLower(r.Spec.AclRules) - } - if r.Spec.AccountType == Custom { - existsAcl := false - for _, v := range strings.Split(r.Spec.AclRules, " ") { - if v == "-acl" || v == "-@admin" || v == "-@all" || v == "-@dangerous" { - existsAcl = true - break - } - } - if !existsAcl { - r.Spec.AclRules = fmt.Sprintf("%s -acl", r.Spec.AclRules) - } - } - - switch r.Spec.Arch { - case redis.SentinelArch: - rf := &databasesv1.RedisFailover{} - _err := dyClient.Get(context.Background(), types.NamespacedName{ - Namespace: r.Namespace, - Name: r.Spec.RedisName}, rf) - if _err != nil { - redislog.Error(_err, "get redis failover failed", "name", r.Name) - } else { - r.OwnerReferences = util.BuildOwnerReferencesWithParents(rf) - } - case redis.ClusterArch: - cluster := &clusterv1.DistributedRedisCluster{} - - _err := dyClient.Get(context.Background(), types.NamespacedName{ - Namespace: r.Namespace, - Name: r.Spec.RedisName}, cluster) - if _err != nil { - redislog.Error(_err, "get redis cluster failed", "name", r.Name) - } else { - r.OwnerReferences = util.BuildOwnerReferencesWithParents(cluster) - } - } -} - -// ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (r *RedisUser) ValidateCreate() (admission.Warnings, error) { - redislog.V(3).Info("validate create", "name", r.Name) - if r.GetDeletionTimestamp() != nil { - return nil, nil - } - if r.Spec.AccountType == Custom { - err := util.CheckUserRuleUpdate(r.Spec.AclRules) - if err != nil { - return nil, fmt.Errorf("acl rules is invalid: %v", err) - } - } - if r.Spec.AccountType == System { - if r.Spec.Username != "operator" { - return nil, fmt.Errorf("system account username must be operator") - } - return nil, nil - } - if r.Spec.AccountType == Default { - if r.Spec.Username != "default" { - return nil, fmt.Errorf("default account username must be default") - } - err := util.CheckRule(r.Spec.AclRules) - if err != nil { - return nil, fmt.Errorf("acl rules is invalid: %v", err) - } - for _, v := range r.Spec.PasswordSecrets { - if v == "" { - continue - } - _secret := &v1.Secret{} - err := dyClient.Get(context.Background(), types.NamespacedName{ - Namespace: r.Namespace, - Name: v, - }, _secret) - if err != nil { - return nil, err - } else { - if v, ok := _secret.Data["password"]; len(v) == 0 || !ok { - return nil, fmt.Errorf("password secret key password is empty or no exists") - } - } - } - return nil, nil - } - - switch r.Spec.Arch { - case redis.SentinelArch: - rf := &databasesv1.RedisFailover{} - _err := dyClient.Get(context.Background(), types.NamespacedName{ - Namespace: r.Namespace, - Name: r.Spec.RedisName}, rf) - if _err != nil { - return nil, _err - } - if rf.Status.Phase != databasesv1.PhaseReady { - return nil, fmt.Errorf("redis failover %s is not ready", r.Spec.RedisName) - } - case redis.ClusterArch: - cluster := clusterv1.DistributedRedisCluster{} - _err := dyClient.Get(context.Background(), types.NamespacedName{ - Namespace: r.Namespace, - Name: r.Spec.RedisName}, &cluster) - if _err != nil { - return nil, _err - } - if cluster.Status.Status != clusterv1.ClusterStatusOK { - return nil, fmt.Errorf("redis cluster %s is not ready", r.Spec.RedisName) - } - } - - err := util.CheckRule(r.Spec.AclRules) - if err != nil { - return nil, fmt.Errorf("acl rules is invalid: %v", err) - } - - return nil, nil -} - -// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (r *RedisUser) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { - if r.Spec.AccountType == System { - return nil, nil - } - if r.GetDeletionTimestamp() != nil { - return nil, nil - } - if !controllerutil.ContainsFinalizer(r, RedisUserFinalizer) { - return nil, nil - } - redislog.V(3).Info("validate update", "name", r.Name) - err := util.CheckRule(r.Spec.AclRules) - if err != nil { - return nil, fmt.Errorf("acl rules is invalid: %v", err) - } - if err := util.CheckUserRuleUpdate(r.Spec.AclRules); err != nil { - return nil, fmt.Errorf("acl rules is invalid: %v", err) - } - - switch r.Spec.Arch { - case redis.SentinelArch: - rf := &databasesv1.RedisFailover{} - err = dyClient.Get(context.Background(), types.NamespacedName{ - Namespace: r.Namespace, - Name: r.Spec.RedisName}, rf) - if err != nil { - return nil, err - } - if rf.Status.Phase != databasesv1.PhaseReady { - return nil, fmt.Errorf("redis failover %s is not ready", r.Spec.RedisName) - } - case redis.ClusterArch: - cluster := clusterv1.DistributedRedisCluster{} - err = dyClient.Get(context.Background(), types.NamespacedName{ - Namespace: r.Namespace, - Name: r.Spec.RedisName}, &cluster) - if err != nil { - return nil, err - } - if cluster.Status.Status != clusterv1.ClusterStatusOK { - return nil, fmt.Errorf("redis cluster %s is not ready", r.Spec.RedisName) - } - } - return nil, nil -} - -// ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (r *RedisUser) ValidateDelete() (admission.Warnings, error) { - return nil, nil -} diff --git a/api/redis/v1/validate.go b/api/redis/v1/validate.go deleted file mode 100644 index 0969f26..0000000 --- a/api/redis/v1/validate.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1 - -import ( - "fmt" - "os" - "path" - "time" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -// Validate set the values by default if not defined and checks if the values given are valid -func (r *RedisClusterBackup) Validate() error { - if r.Spec.Source.RedisClusterName == "" { - return fmt.Errorf("RedisFailoverName is not valid") - } - if r.Spec.Storage.IsZero() { - return fmt.Errorf("backup storage can't be empty") - } - if r.Spec.Target.S3Option.S3Secret != "" { - r.Spec.Source.Endpoint = []IpPort{{ - Address: fmt.Sprintf("%s-0", r.Spec.Source.RedisClusterName), - Port: 6379, - }, { - Address: fmt.Sprintf("%s-1", r.Spec.Source.RedisClusterName), - Port: 6379, - }, { - Address: fmt.Sprintf("%s-2", r.Spec.Source.RedisClusterName), - Port: 6379, - }} - if r.Spec.Target.S3Option.Dir == "" { - currentTime := time.Now().UTC() - timeString := currentTime.Format("2006-01-02T15:04:05Z") - r.Spec.Target.S3Option.Dir = path.Join("data", "backup", "redis-cluster", "manual", timeString) - } - if r.Spec.Image == "" { - r.Spec.Image = os.Getenv("REDIS_TOOLS_IMAGE") - } - if r.Spec.BackoffLimit == nil { - r.Spec.BackoffLimit = new(int32) - *r.Spec.BackoffLimit = 1 - } - } - if r.Spec.Resources == nil { - r.Spec.Resources = &corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("500Mi"), - corev1.ResourceCPU: resource.MustParse("500m"), - }, - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("500m"), - }, - } - } - return nil -} diff --git a/api/redis/v1/zz_generated.deepcopy.go b/api/redis/v1/zz_generated.deepcopy.go deleted file mode 100644 index 20ff838..0000000 --- a/api/redis/v1/zz_generated.deepcopy.go +++ /dev/null @@ -1,540 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by controller-gen. DO NOT EDIT. - -package v1 - -import ( - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in BackupSorterByCreateTime) DeepCopyInto(out *BackupSorterByCreateTime) { - { - in := &in - *out = make(BackupSorterByCreateTime, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSorterByCreateTime. -func (in BackupSorterByCreateTime) DeepCopy() BackupSorterByCreateTime { - if in == nil { - return nil - } - out := new(BackupSorterByCreateTime) - in.DeepCopyInto(out) - return *out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterRedisBackupSource) DeepCopyInto(out *ClusterRedisBackupSource) { - *out = *in - if in.Endpoint != nil { - in, out := &in.Endpoint, &out.Endpoint - *out = make([]IpPort, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRedisBackupSource. -func (in *ClusterRedisBackupSource) DeepCopy() *ClusterRedisBackupSource { - if in == nil { - return nil - } - out := new(ClusterRedisBackupSource) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GRPCOption) DeepCopyInto(out *GRPCOption) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCOption. -func (in *GRPCOption) DeepCopy() *GRPCOption { - if in == nil { - return nil - } - out := new(GRPCOption) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IpPort) DeepCopyInto(out *IpPort) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IpPort. -func (in *IpPort) DeepCopy() *IpPort { - if in == nil { - return nil - } - out := new(IpPort) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackup) DeepCopyInto(out *RedisBackup) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackup. -func (in *RedisBackup) DeepCopy() *RedisBackup { - if in == nil { - return nil - } - out := new(RedisBackup) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RedisBackup) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupList) DeepCopyInto(out *RedisBackupList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]RedisBackup, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupList. -func (in *RedisBackupList) DeepCopy() *RedisBackupList { - if in == nil { - return nil - } - out := new(RedisBackupList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RedisBackupList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupSource) DeepCopyInto(out *RedisBackupSource) { - *out = *in - if in.Endpoint != nil { - in, out := &in.Endpoint, &out.Endpoint - *out = make([]IpPort, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupSource. -func (in *RedisBackupSource) DeepCopy() *RedisBackupSource { - if in == nil { - return nil - } - out := new(RedisBackupSource) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupSpec) DeepCopyInto(out *RedisBackupSpec) { - *out = *in - in.Source.DeepCopyInto(&out.Source) - out.Storage = in.Storage.DeepCopy() - out.Target = in.Target - in.Resources.DeepCopyInto(&out.Resources) - if in.BackoffLimit != nil { - in, out := &in.BackoffLimit, &out.BackoffLimit - *out = new(int32) - **out = **in - } - if in.ActiveDeadlineSeconds != nil { - in, out := &in.ActiveDeadlineSeconds, &out.ActiveDeadlineSeconds - *out = new(int64) - **out = **in - } - if in.NodeSelector != nil { - in, out := &in.NodeSelector, &out.NodeSelector - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.SecurityContext != nil { - in, out := &in.SecurityContext, &out.SecurityContext - *out = new(corev1.PodSecurityContext) - (*in).DeepCopyInto(*out) - } - if in.ImagePullSecrets != nil { - in, out := &in.ImagePullSecrets, &out.ImagePullSecrets - *out = make([]corev1.LocalObjectReference, len(*in)) - copy(*out, *in) - } - if in.Tolerations != nil { - in, out := &in.Tolerations, &out.Tolerations - *out = make([]corev1.Toleration, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Affinity != nil { - in, out := &in.Affinity, &out.Affinity - *out = new(corev1.Affinity) - (*in).DeepCopyInto(*out) - } - if in.RestoreCreateAt != nil { - in, out := &in.RestoreCreateAt, &out.RestoreCreateAt - *out = (*in).DeepCopy() - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupSpec. -func (in *RedisBackupSpec) DeepCopy() *RedisBackupSpec { - if in == nil { - return nil - } - out := new(RedisBackupSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupStatus) DeepCopyInto(out *RedisBackupStatus) { - *out = *in - if in.StartTime != nil { - in, out := &in.StartTime, &out.StartTime - *out = (*in).DeepCopy() - } - if in.CompletionTime != nil { - in, out := &in.CompletionTime, &out.CompletionTime - *out = (*in).DeepCopy() - } - if in.LastCheckTime != nil { - in, out := &in.LastCheckTime, &out.LastCheckTime - *out = (*in).DeepCopy() - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupStatus. -func (in *RedisBackupStatus) DeepCopy() *RedisBackupStatus { - if in == nil { - return nil - } - out := new(RedisBackupStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisBackupTarget) DeepCopyInto(out *RedisBackupTarget) { - *out = *in - out.S3Option = in.S3Option - out.GRPCOption = in.GRPCOption -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisBackupTarget. -func (in *RedisBackupTarget) DeepCopy() *RedisBackupTarget { - if in == nil { - return nil - } - out := new(RedisBackupTarget) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisClusterBackup) DeepCopyInto(out *RedisClusterBackup) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisClusterBackup. -func (in *RedisClusterBackup) DeepCopy() *RedisClusterBackup { - if in == nil { - return nil - } - out := new(RedisClusterBackup) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RedisClusterBackup) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisClusterBackupList) DeepCopyInto(out *RedisClusterBackupList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]RedisClusterBackup, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisClusterBackupList. -func (in *RedisClusterBackupList) DeepCopy() *RedisClusterBackupList { - if in == nil { - return nil - } - out := new(RedisClusterBackupList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RedisClusterBackupList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisClusterBackupSpec) DeepCopyInto(out *RedisClusterBackupSpec) { - *out = *in - in.Source.DeepCopyInto(&out.Source) - out.Storage = in.Storage.DeepCopy() - out.Target = in.Target - if in.BackoffLimit != nil { - in, out := &in.BackoffLimit, &out.BackoffLimit - *out = new(int32) - **out = **in - } - if in.ActiveDeadlineSeconds != nil { - in, out := &in.ActiveDeadlineSeconds, &out.ActiveDeadlineSeconds - *out = new(int64) - **out = **in - } - if in.SecurityContext != nil { - in, out := &in.SecurityContext, &out.SecurityContext - *out = new(corev1.PodSecurityContext) - (*in).DeepCopyInto(*out) - } - if in.ImagePullSecrets != nil { - in, out := &in.ImagePullSecrets, &out.ImagePullSecrets - *out = make([]corev1.LocalObjectReference, len(*in)) - copy(*out, *in) - } - if in.Tolerations != nil { - in, out := &in.Tolerations, &out.Tolerations - *out = make([]corev1.Toleration, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Affinity != nil { - in, out := &in.Affinity, &out.Affinity - *out = new(corev1.Affinity) - (*in).DeepCopyInto(*out) - } - if in.NodeSelector != nil { - in, out := &in.NodeSelector, &out.NodeSelector - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = new(corev1.ResourceRequirements) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisClusterBackupSpec. -func (in *RedisClusterBackupSpec) DeepCopy() *RedisClusterBackupSpec { - if in == nil { - return nil - } - out := new(RedisClusterBackupSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisClusterBackupStatus) DeepCopyInto(out *RedisClusterBackupStatus) { - *out = *in - if in.StartTime != nil { - in, out := &in.StartTime, &out.StartTime - *out = (*in).DeepCopy() - } - if in.CompletionTime != nil { - in, out := &in.CompletionTime, &out.CompletionTime - *out = (*in).DeepCopy() - } - if in.LastCheckTime != nil { - in, out := &in.LastCheckTime, &out.LastCheckTime - *out = (*in).DeepCopy() - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisClusterBackupStatus. -func (in *RedisClusterBackupStatus) DeepCopy() *RedisClusterBackupStatus { - if in == nil { - return nil - } - out := new(RedisClusterBackupStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisUser) DeepCopyInto(out *RedisUser) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUser. -func (in *RedisUser) DeepCopy() *RedisUser { - if in == nil { - return nil - } - out := new(RedisUser) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RedisUser) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisUserList) DeepCopyInto(out *RedisUserList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]RedisUser, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUserList. -func (in *RedisUserList) DeepCopy() *RedisUserList { - if in == nil { - return nil - } - out := new(RedisUserList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RedisUserList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisUserSpec) DeepCopyInto(out *RedisUserSpec) { - *out = *in - if in.PasswordSecrets != nil { - in, out := &in.PasswordSecrets, &out.PasswordSecrets - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUserSpec. -func (in *RedisUserSpec) DeepCopy() *RedisUserSpec { - if in == nil { - return nil - } - out := new(RedisUserSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisUserStatus) DeepCopyInto(out *RedisUserStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisUserStatus. -func (in *RedisUserStatus) DeepCopy() *RedisUserStatus { - if in == nil { - return nil - } - out := new(RedisUserStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *S3Option) DeepCopyInto(out *S3Option) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Option. -func (in *S3Option) DeepCopy() *S3Option { - if in == nil { - return nil - } - out := new(S3Option) - in.DeepCopyInto(out) - return out -} diff --git a/cmd/main.go b/cmd/main.go index cfc55a6..85b39f7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,33 +17,44 @@ limitations under the License. package main import ( + "context" + "crypto/tls" "flag" "os" + "strings" + "time" - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - clusterv1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - middlewarev1 "github.com/alauda/redis-operator/api/redis/v1" - - redisfailovercontrollers "github.com/alauda/redis-operator/internal/controller/databases.spotahome.com" - middlewarecontrollers "github.com/alauda/redis-operator/internal/controller/redis" - redismiddlewarealaudaiocontrollers "github.com/alauda/redis-operator/internal/controller/redis" - rediskuncontrollers "github.com/alauda/redis-operator/internal/controller/redis.kun" + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + middlewareredisv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + middlewarev1 "github.com/alauda/redis-operator/api/middleware/v1" + "github.com/alauda/redis-operator/internal/config" + rediskuncontrollers "github.com/alauda/redis-operator/internal/controller/cluster" + databasescontroller "github.com/alauda/redis-operator/internal/controller/databases" + middlewarecontrollers "github.com/alauda/redis-operator/internal/controller/middleware" + redismiddlewarealaudaiocontrollers "github.com/alauda/redis-operator/internal/controller/middleware" + "github.com/alauda/redis-operator/internal/ops" + _ "github.com/alauda/redis-operator/internal/ops/cluster/actor" + _ "github.com/alauda/redis-operator/internal/ops/failover/actor" + _ "github.com/alauda/redis-operator/internal/ops/sentinel/actor" + "github.com/alauda/redis-operator/internal/vc" "github.com/alauda/redis-operator/pkg/actor" "github.com/alauda/redis-operator/pkg/kubernetes/clientset" - "github.com/alauda/redis-operator/pkg/ops" - cactor "github.com/alauda/redis-operator/pkg/ops/cluster/actor" - sactor "github.com/alauda/redis-operator/pkg/ops/sentinel/actor" certv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" - smv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "github.com/samber/lo" "go.uber.org/zap/zapcore" coordination "k8s.io/api/coordination/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" //+kubebuilder:scaffold:imports ) @@ -53,15 +64,14 @@ var ( ) func init() { + utilruntime.Must(coordination.AddToScheme(scheme)) utilruntime.Must(certv1.AddToScheme(scheme)) utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(databasesv1.AddToScheme(scheme)) - utilruntime.Must(middlewarev1.AddToScheme(scheme)) - utilruntime.Must(smv1.AddToScheme(scheme)) + utilruntime.Must(clusterv1.AddToScheme(scheme)) + utilruntime.Must(databasesv1.AddToScheme(scheme)) utilruntime.Must(middlewarev1.AddToScheme(scheme)) - utilruntime.Must(coordination.AddToScheme(scheme)) - + utilruntime.Must(middlewareredisv1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -74,12 +84,11 @@ func init() { //+kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets;poddisruptionbudgets/finalizers,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=*,resources=pods;pods/exec;configmaps;endpoints;services;services/finalizers,verbs=* - //+kubebuilder:rbac:groups=*,resources=nodes,verbs=get;list;watch //+kubebuilder:rbac:groups=*,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;clusterroles;rolebindings;clusterrolebindings,verbs=get;list;watch;create;update;patch;delete - //+kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete + // referral //+kubebuilder:rbac:groups=redis.kun,resources=*,verbs=* //+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=*,verbs=* @@ -89,11 +98,14 @@ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string + var secureMetrics bool + var enableHTTP2 bool flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, - "Enable leader election for controller manager. "+ - "Enabling this will ensure there is only one active controller manager.") + "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", false, "If set the metrics endpoint is served securely") + flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") opts := zap.Options{ Development: true, StacktraceLevel: zapcore.FatalLevel, @@ -104,89 +116,110 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + tlsOpts := []func(*tls.Config){} + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + webhookServer := webhook.NewServer(webhook.Options{TLSOpts: tlsOpts, Port: 9443}) + cacheOpts := cache.Options{Scheme: scheme} + + var namespaces []string + { + watchedNamespaces := lo.FirstOr([]string{os.Getenv("WATCH_ALL_NAMESPACES")}, os.Getenv("WATCH_NAMESPACE")) + if watchedNamespaces != "" { + namespaces = strings.Split(watchedNamespaces, ",") + cacheOpts.DefaultNamespaces = map[string]cache.Config{} + for _, ns := range namespaces { + cacheOpts.DefaultNamespaces[ns] = cache.Config{} + } + } + } + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, - Namespace: os.Getenv("WATCH_NAMESPACE"), - MetricsBindAddress: metricsAddr, - Port: 9443, + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + }, + Cache: cacheOpts, + WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "926185dc.alauda.cn", }) - if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } - eventRecorder := mgr.GetEventRecorderFor("redis-operator") + noCacheCli, err := client.New(mgr.GetConfig(), client.Options{Scheme: scheme}) + if err != nil { + setupLog.Error(err, "unable to create client") + os.Exit(1) + } + eventRecorder := mgr.GetEventRecorderFor("redis-operator") // init actor - clientset := clientset.NewWithConfig(mgr.GetClient(), mgr.GetConfig(), mgr.GetLogger()) - - actorManager := actor.NewActorManager() - _ = actorManager.Add(sactor.NewEnsureResourceActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(sactor.NewSentinelHealActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(sactor.NewActorHealMasterActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(sactor.NewActorInitMasterActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(sactor.NewSentinelUpdateConfig(clientset, mgr.GetLogger())) - _ = actorManager.Add(sactor.NewPatchLabelsActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(sactor.NewUpdateAccountActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewCleanResourceActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewEnsureResourceActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewEnsureSlotsActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewHealPodActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewJoinNodeActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewRebalanceActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewUpdateConfigActor(clientset, mgr.GetLogger())) - _ = actorManager.Add(cactor.NewUpdateAccountActor(clientset, mgr.GetLogger())) - - engine, err := ops.NewOpEngine(mgr.GetClient(), eventRecorder, actorManager, mgr.GetLogger()) + clientset := clientset.NewWithConfig(noCacheCli, mgr.GetConfig(), mgr.GetLogger()) + actorManager := actor.NewActorManager(clientset, setupLog.WithName("ActorManager")) + engine, err := ops.NewOpEngine(noCacheCli, eventRecorder, actorManager, mgr.GetLogger()) if err != nil { setupLog.Error(err, "init op engine failed") os.Exit(1) } - if err = (&middlewarecontrollers.RedisBackupReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Logger: mgr.GetLogger(), + if err = (&databasescontroller.RedisFailoverReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: eventRecorder, + Engine: engine, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "RedisBackup") + setupLog.Error(err, "unable to create controller", "controller", "RedisFailover") os.Exit(1) } - - if err = (&rediskuncontrollers.DistributedRedisClusterReconciler{ + if err = (&databasescontroller.RedisSentinelReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), EventRecorder: eventRecorder, Engine: engine, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "DistributedRedisCluster") + setupLog.Error(err, "unable to create controller", "controller", "RedisSentinel") os.Exit(1) } - if err = (&redisfailovercontrollers.RedisFailoverReconciler{ + if err = (&rediskuncontrollers.DistributedRedisClusterReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), EventRecorder: eventRecorder, Engine: engine, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "RedisFailover") + setupLog.Error(err, "unable to create controller", "controller", "DistributedRedisCluster") os.Exit(1) } - if err = (&middlewarecontrollers.RedisClusterBackupReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Logger: mgr.GetLogger(), + if err = (&middlewarecontrollers.RedisReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Logger: mgr.GetLogger(), + ActorManager: actorManager, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "RedisClusterBackup") + setupLog.Error(err, "unable to create controller", "controller", "Redis") os.Exit(1) } - - if err = (&middlewarev1.RedisUser{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "RedisUser") + if err = (&middlewarev1.Redis{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Redis") os.Exit(1) } @@ -200,7 +233,12 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "RedisUser") os.Exit(1) } + if err = (&middlewareredisv1.RedisUser{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "RedisUser") + os.Exit(1) + } //+kubebuilder:scaffold:builder + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) @@ -210,7 +248,42 @@ func main() { os.Exit(1) } + if err := func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + logger := setupLog.WithName("BundleVersion") + setupLog.Info("check old bundle version") + if len(namespaces) > 0 { + for _, namespace := range namespaces { + if err := vc.InstallOldImageVersion(ctx, noCacheCli, namespace, logger); err != nil { + setupLog.Error(err, "install old image version failed") + return err + } + } + } else { + if err := vc.InstallOldImageVersion(ctx, noCacheCli, "", logger); err != nil { + setupLog.Error(err, "install old image version failed") + return err + } + } + setupLog.Info("install current bundle version", "version", config.GetOperatorVersion()) + if err := vc.InstallCurrentImageVersion(ctx, noCacheCli, logger); err != nil { + setupLog.Error(err, "install current image version failed") + return err + } + setupLog.Info("reset latest bundle version") + if err := vc.CleanImageVersions(ctx, noCacheCli, logger); err != nil { + setupLog.Error(err, "clean bundle versions failed") + return err + } + return nil + }(); err != nil { + os.Exit(1) + } + setupLog.Info("starting manager") + actorManager.Print() if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) diff --git a/cmd/redis-tools/README.md b/cmd/redis-tools/README.md index d07bf33..c28dc3e 100644 --- a/cmd/redis-tools/README.md +++ b/cmd/redis-tools/README.md @@ -2,10 +2,10 @@ This is the clone of private repo which used to manage Redis pods. Supported subcommands as below: -| SubCommand | Description | -|:-----------|:----------------------------| -| cluster | cluster releated functions | -| sentinel | sentinel releated functions | -| helper | helper functions | -| runner | daemonset functions | -| backup | backup command | +| SubCommand | Description | +|:-----------|:----------------------------------| +| cluster | redis cluster releated tools | +| failover | redis failover releated tools | +| sentinel | redis sentinel releated tools | +| helper | helper functions | +| runner | daemonset functions | diff --git a/cmd/redis-tools/pkg/commands/cluster/command.go b/cmd/redis-tools/commands/cluster/command.go similarity index 73% rename from cmd/redis-tools/pkg/commands/cluster/command.go rename to cmd/redis-tools/commands/cluster/command.go index 184428b..9704cee 100644 --- a/cmd/redis-tools/pkg/commands/cluster/command.go +++ b/cmd/redis-tools/commands/cluster/command.go @@ -1,35 +1,19 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package cluster import ( "context" "time" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/kubernetes/client" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/logger" + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/cmd/redis-tools/util" "github.com/urfave/cli/v2" + corev1 "k8s.io/api/core/v1" ) func NewCommand(ctx context.Context) *cli.Command { return &cli.Command{ Name: "cluster", - Usage: "cluster set commands", + Usage: "Cluster set commands", Flags: []cli.Flag{ &cli.StringFlag{ Name: "namespace", @@ -111,6 +95,12 @@ func NewCommand(ctx context.Context) *cli.Command { EnvVars: []string{"CUSTOM_PORT_ENABLED"}, Value: "false", }, + &cli.StringFlag{ + Name: "service-type", + Usage: "Service type for sentinel service", + EnvVars: []string{"SERVICE_TYPE"}, + Value: "ClusterIP", + }, }, Subcommands: []*cli.Command{ { @@ -119,11 +109,10 @@ func NewCommand(ctx context.Context) *cli.Command { Flags: []cli.Flag{}, Action: func(c *cli.Context) error { var ( - namespace = c.String("namespace") - podName = c.String("pod-name") - ipFamily = c.String("ip-family") - customPort = c.String("custom-port-enabled") == "true" - nodeportEnabled = c.String("nodeport-enabled") == "true" + namespace = c.String("namespace") + podName = c.String("pod-name") + ipFamily = c.String("ip-family") + serviceType = corev1.ServiceType(c.String("service-type")) ) if namespace == "" { return cli.Exit("require namespace", 1) @@ -132,15 +121,15 @@ func NewCommand(ctx context.Context) *cli.Command { return cli.Exit("require podname", 1) } - logger := logger.NewLogger(c).WithName("Expose") + logger := util.NewLogger(c).WithName("Expose") - client, err := client.NewClient() + client, err := util.NewClient() if err != nil { logger.Error(err, "create k8s client failed, error=%s", err) return cli.Exit(err, 1) } - if err := ExposeNodePort(ctx, client, namespace, podName, ipFamily, nodeportEnabled, customPort, logger); err != nil { + if err := ExposeNodePort(ctx, client, namespace, podName, ipFamily, serviceType, logger); err != nil { logger.Error(err, "expose node port failed") return cli.Exit(err, 1) } @@ -158,7 +147,7 @@ func NewCommand(ctx context.Context) *cli.Command { Value: "/data", }, &cli.StringFlag{ - Name: "node-config-name", + Name: "config-name", Usage: "Node config file name", Value: "nodes.conf", }, @@ -167,11 +156,16 @@ func NewCommand(ctx context.Context) *cli.Command { Usage: "Configmap name prefix", Value: "sync-", }, + &cli.StringFlag{ + Name: "shard-id", + Usage: "Shard ID for redis cluster shard", + EnvVars: []string{"SHARD_ID"}, + }, }, Action: func(c *cli.Context) error { - logger := logger.NewLogger(c).WithName("Heal") + logger := util.NewLogger(c).WithName("Heal") - client, err := client.NewClient() + client, err := util.NewClient() if err != nil { logger.Error(err, "create k8s client failed, error=%s", err) return cli.Exit(err, 1) @@ -208,7 +202,7 @@ func NewCommand(ctx context.Context) *cli.Command { serviceAddr = c.String("addr") timeout = c.Int64("timeout") ) - logger := logger.NewLogger(c).WithName("readiness") + logger := util.NewLogger(c).WithName("readiness") if timeout <= 0 { timeout = 4 @@ -233,33 +227,6 @@ func NewCommand(ctx context.Context) *cli.Command { return nil }, }, - { - Name: "proxy_check", - Usage: "proxy check ", - Action: func(c *cli.Context) error { - var ( - serviceAddr = "127.0.0.1:6379" - timeout = c.Int64("timeout") - ) - logger := logger.NewLogger(c).WithName("proxy_check") - - if timeout <= 0 { - timeout = 4 - } - ctx, cancel := context.WithTimeout(ctx, time.Second*time.Duration(timeout)) - defer cancel() - - info, err := commands.LoadProxyAuthInfo(c, ctx) - if err != nil { - logger.Error(err, "load auth info failed") - } - if err := Check(ctx, serviceAddr, *info); err != nil { - logger.Error(err, "check proxy failed") - return cli.Exit(err, 1) - } - return nil - }, - }, { Name: "liveness", Usage: "Redis node liveness check, which just checked tcp socket", @@ -268,7 +235,7 @@ func NewCommand(ctx context.Context) *cli.Command { serviceAddr = c.String("addr") timeout = c.Int64("timeout") ) - logger := logger.NewLogger(c).WithName("liveness") + logger := util.NewLogger(c).WithName("liveness") if timeout <= 0 { timeout = 5 @@ -296,10 +263,15 @@ func NewCommand(ctx context.Context) *cli.Command { Value: "/data", }, &cli.StringFlag{ - Name: "node-config-name", + Name: "config-name", Usage: "Node config file name", Value: "nodes.conf", }, + &cli.StringFlag{ + Name: "prefix", + Usage: "Configmap name prefix", + Value: "sync-", + }, &cli.IntFlag{ Name: "timeout", Aliases: []string{"t"}, @@ -308,9 +280,9 @@ func NewCommand(ctx context.Context) *cli.Command { }, }, Action: func(c *cli.Context) error { - logger := logger.NewLogger(c).WithName("Shutdown") + logger := util.NewLogger(c).WithName("Shutdown") - client, err := client.NewClient() + client, err := util.NewClient() if err != nil { logger.Error(err, "create k8s client failed, error=%s", err) return cli.Exit(err, 1) diff --git a/cmd/redis-tools/commands/cluster/expose.go b/cmd/redis-tools/commands/cluster/expose.go new file mode 100644 index 0000000..ff80cb7 --- /dev/null +++ b/cmd/redis-tools/commands/cluster/expose.go @@ -0,0 +1,168 @@ +package cluster + +import ( + "context" + "fmt" + "net/netip" + "os" + "strconv" + "strings" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/client-go/kubernetes" +) + +// ExposeNodePort +func ExposeNodePort(ctx context.Context, client *kubernetes.Clientset, namespace, podName, ipfamily string, serviceType corev1.ServiceType, logger logr.Logger) error { + logger.Info("Info", "serviceType", serviceType, "ipfamily", ipfamily) + pod, err := commands.GetPod(ctx, client, namespace, podName, logger) + if err != nil { + logger.Error(err, "get pods failed", "namespace", namespace, "name", podName) + return err + } + if pod.Status.HostIP == "" { + return fmt.Errorf("pod not found or pod with invalid hostIP") + } + + var ( + announceIp = pod.Status.PodIP + announcePort int32 = 6379 + // announceIPort is this port necessary ? + announceIPort int32 = 16379 + ) + if serviceType == corev1.ServiceTypeNodePort { + podSvc, err := commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeNodePort, 20, logger) + if errors.IsNotFound(err) { + if podSvc, err = commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeNodePort, 20, logger); err != nil { + logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + } else if err != nil { + logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + for _, v := range podSvc.Spec.Ports { + if v.Name == "client" { + announcePort = v.NodePort + } + if v.Name == "gossip" { + announceIPort = v.NodePort + } + } + + node, err := client.CoreV1().Nodes().Get(ctx, pod.Spec.NodeName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "get nodes err", "node", node.Name) + return err + } + logger.Info("get nodes success", "Name", node.Name) + + var addresses []string + for _, addr := range node.Status.Addresses { + if addr.Address == "" { + continue + } + + switch addr.Type { + case corev1.NodeExternalIP: + ip, err := netip.ParseAddr(addr.Address) + if err != nil { + logger.Error(err, "parse address err", "address", addr.Address) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + addresses = append(addresses, addr.Address) + } else if ipfamily != "IPv6" && ip.Is4() { + addresses = append(addresses, addr.Address) + } + case corev1.NodeInternalIP: + // internal ip first + ip, err := netip.ParseAddr(addr.Address) + if err != nil { + logger.Error(err, "parse address err", "address", addr.Address) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + addresses = append([]string{addr.Address}, addresses...) + addresses = append(addresses, addr.Address) + } else if ipfamily != "IPv6" && ip.Is4() { + addresses = append([]string{addr.Address}, addresses...) + } + } + } + if len(addresses) > 0 { + announceIp = addresses[0] + } else { + err := fmt.Errorf("no available address") + logger.Error(err, "get usable address failed") + return err + } + } else if serviceType == corev1.ServiceTypeLoadBalancer { + podSvc, err := commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeLoadBalancer, 20, logger) + if errors.IsNotFound(err) { + if podSvc, err = commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeLoadBalancer, 20, logger); err != nil { + logger.Error(err, "retry get lb service failed") + return err + } + } else if err != nil { + logger.Error(err, "get lb service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + + for _, v := range podSvc.Status.LoadBalancer.Ingress { + if v.IP != "" { + ip, err := netip.ParseAddr(v.IP) + if err != nil { + logger.Error(err, "parse address err", "address", v.IP) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + announceIp = v.IP + break + } + if ipfamily != "IPv6" && ip.Is4() { + announceIp = v.IP + break + } + } + } + } else { + for _, addr := range pod.Status.PodIPs { + ip, err := netip.ParseAddr(addr.IP) + if err != nil { + return err + } + if ipfamily == "IPv6" && ip.Is6() { + announceIp = addr.IP + break + } else if ipfamily != "IPv6" && ip.Is4() { + announceIp = addr.IP + break + } + } + } + + format_announceIp := strings.Replace(announceIp, ":", "-", -1) + labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s"},{"op":"add","path":"/metadata/labels/%s","value":"%s"},{"op":"add","path":"/metadata/labels/%s","value":"%s"}]`, + strings.Replace("middleware.alauda.io/announce_ip", "/", "~1", -1), format_announceIp, + strings.Replace("middleware.alauda.io/announce_port", "/", "~1", -1), strconv.Itoa(int(announcePort)), + strings.Replace("middleware.alauda.io/announce_iport", "/", "~1", -1), strconv.Itoa(int(announceIPort))) + + logger.Info(labelPatch) + _, err = client.CoreV1().Pods(pod.Namespace).Patch(ctx, podName, types.JSONPatchType, []byte(labelPatch), metav1.PatchOptions{}) + if err != nil { + logger.Error(err, "patch pod label failed") + return err + } + configContent := fmt.Sprintf(`cluster-announce-ip %s +cluster-announce-port %d +cluster-announce-bus-port %d`, announceIp, announcePort, announceIPort) + + return os.WriteFile("/data/announce.conf", []byte(configContent), 0644) // #nosec: G306 +} diff --git a/cmd/redis-tools/commands/cluster/heal.go b/cmd/redis-tools/commands/cluster/heal.go new file mode 100644 index 0000000..0149e96 --- /dev/null +++ b/cmd/redis-tools/commands/cluster/heal.go @@ -0,0 +1,425 @@ +package cluster + +import ( + "context" + "crypto/sha1" // #nosec G505 + "fmt" + "net" + "os" + "path" + "strings" + "time" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/pkg/redis" + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type HealOptions struct { + Namespace string + PodName string + Workspace string + TargetName string + Prefix string + ShardID string + NodeFile string +} + +// Heal heal may fail when updated password for redis 4,5 +func Heal(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { + opts := &HealOptions{ + Namespace: c.String("namespace"), + PodName: c.String("pod-name"), + Workspace: c.String("workspace"), + TargetName: c.String("config-name"), + Prefix: c.String("prefix"), + ShardID: c.String("shard-id"), + NodeFile: path.Join(c.String("workspace"), c.String("config-name")), + } + + var ( + err error + data []byte + ) + + logger.Info(fmt.Sprintf("check cluster status before pod %s startup", opts.PodName)) + if data, err = initClusterNodesConf(ctx, client, logger, opts); err != nil { + logger.Error(err, "nit nodes.conf failed") + return err + } + if data, err = portClusterNodesConf(ctx, data, logger, opts); err != nil { + logger.Error(err, "port nodes.conf failed") + return err + } + + // persistent + nodeFileBak := opts.NodeFile + ".bak" + // back the nodes.conf to nodes.conf.bak + _ = os.Rename(opts.NodeFile, nodeFileBak) + + tmpFile := path.Join(opts.Workspace, "tmp-"+opts.TargetName) + if err := os.WriteFile(tmpFile, []byte(data), 0600); err != nil { + logger.Error(err, "update nodes.conf failed") + return err + } else if err := os.Rename(tmpFile, opts.NodeFile); err != nil { + logger.Error(err, "rename tmp-nodes.conf to nodes.conf failed") + return err + } + return healCluster(c, ctx, client, data, logger) +} + +func initClusterNodesConf(ctx context.Context, client *kubernetes.Clientset, logger logr.Logger, opts *HealOptions) ([]byte, error) { + var ( + isUpdated = false + data, err = os.ReadFile(opts.NodeFile) + ) + if err != nil { + if !os.IsNotExist(err) { + logger.Error(err, "get nodes.conf failed") + return nil, err + } + + configName := strings.Join([]string{strings.TrimSuffix(opts.Prefix, "-"), opts.PodName}, "-") + if data, err = getConfigMapData(ctx, client, opts.Namespace, configName, opts.TargetName, logger); err != nil { + logger.Error(err, "sync nodes.conf from configmap to local failed") + return nil, err + } + isUpdated = true + } + if len(data) == 0 { + // if nodes.conf is empty, custom node-id and shard-id + data = generateRedisCluterNodeRecord(opts.Namespace, opts.PodName, opts.ShardID) + isUpdated = true + } + if isUpdated && len(data) > 0 { + return data, os.WriteFile(opts.NodeFile, data, 0644) // #nosec G306 + } + return data, nil +} + +func portClusterNodesConf(ctx context.Context, data []byte, logger logr.Logger, opts *HealOptions) ([]byte, error) { + var ( + shardId = opts.ShardID + lines []string + epochLine string + ) + + // check node lines + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "vars") { + if epochLine == "" { + epochLine = line + } + continue + } + fields := strings.Fields(line) + // NOTE: here ignore the wrong line, this may caused by redis-server crash + if !strings.Contains(line, "connect") || len(fields) < 8 { + continue + } + lines = append(lines, line) + } + if epochLine != "" { + // format: vars currentEpoch 105 lastVoteEpoch 105 + fields := strings.Fields(epochLine) + if len(fields) == 5 { + lines = append(lines, epochLine) + } else if len(fields) == 4 && fields[3] == "lastVoteEpoch" { + fields = append(fields, fields[2]) + epochLine = strings.Join(fields, " ") + lines = append(lines, epochLine) + } else if len(fields) == 3 && fields[1] == "currentEpoch" { + fields = append(fields, "lastVoteEpoch", fields[2]) + epochLine = strings.Join(fields, " ") + lines = append(lines, epochLine) + } + // ignore epoch line + } + + data = []byte(strings.Join(lines, "\n")) + if shardId == "" { + return data, nil + } + + // verify shard id + lines = lines[0:0] + nodes, err := redis.ParseNodes(string(data)) + if err != nil { + logger.Error(err, "parse nodes failed") + return nil, err + } + selfNode := nodes.Self() + masterNodes := map[string]*redis.ClusterNode{} + for _, node := range nodes.Masters() { + if node.Id == selfNode.Id || + (selfNode.Role == redis.MasterRole && node.MasterId == selfNode.Id) || + (selfNode.Role == redis.SlaveRole && node.Id == selfNode.MasterId) { + continue + } + masterNodes[node.Id] = node + } + + for _, node := range nodes { + line := node.Raw() + if node.Id == selfNode.Id || + (selfNode.Role == redis.MasterRole && node.MasterId == selfNode.Id) || + (selfNode.Role == redis.SlaveRole && node.Id == selfNode.MasterId) { + + if oldShardId := node.AuxFields.ShardID; oldShardId != shardId { + if oldShardId != "" { + line = strings.ReplaceAll(line, fmt.Sprintf("shard-id=%s", oldShardId), fmt.Sprintf("shard-id=%s", shardId)) + } else { + line = strings.ReplaceAll(line, node.AuxFields.Raw(), fmt.Sprintf("%s,,tls-port=0,shard-id=%s", node.AuxFields.Raw(), shardId)) + } + } + line = strings.ReplaceAll(line, ",nofailover", "") + lines = append(lines, line) + } + } + for _, master := range masterNodes { + isAllReplicasInSameShard := true + replicas := nodes.Replicas(master.Id) + for _, replica := range replicas { + if master.AuxFields.ShardID != replica.AuxFields.ShardID { + isAllReplicasInSameShard = false + } + } + if isAllReplicasInSameShard { + lines = append(lines, master.Raw()) + for _, replica := range replicas { + lines = append(lines, replica.Raw()) + } + } + } + + if len(lines) > 0 && epochLine != "" { + lines = append(lines, epochLine) + } + return []byte(strings.Join(lines, "\n")), nil +} + +func healCluster(c *cli.Context, ctx context.Context, client *kubernetes.Clientset, data []byte, logger logr.Logger) error { + var ( + namespace = c.String("namespace") + podName = c.String("pod-name") + ) + if namespace == "" { + return fmt.Errorf("require namespace") + } + if podName == "" { + return fmt.Errorf("require podname") + } + + nodes, err := redis.ParseNodes(string(data)) + if err != nil { + logger.Error(err, "parse nodes failed") + return err + } + nodesInfo, _ := nodes.Marshal() + logger.Info("get nodes", "nodes", string(nodesInfo)) + + if len(nodes) == 0 { + return nil + } + + var ( + self = nodes.Self() + replicas = nodes.Replicas(self.Id) + ) + if !self.IsJoined() || len(replicas) == 0 { + return nil + } + + authInfo, err := commands.LoadAuthInfo(c, ctx) + if err != nil { + logger.Error(err, "load redis operator user info failed") + return err + } + + masterExists := false + // NOTE: when node is in importing state, if do force failover + // some slots will missing, or there will be multi master in cluster + if slots := self.Slots(); !slots.IsImporting() { + pods, _ := getPodsOfShard(ctx, c, client, logger) + for _, pod := range pods { + logger.Info(fmt.Sprintf("check pod %s", pod.GetName())) + if pod.GetName() == podName { + continue + } + if pod.GetDeletionTimestamp() != nil { + continue + } + + if err := func() error { + addr := net.JoinHostPort(pod.Status.PodIP, "6379") + announceIP := strings.ReplaceAll(pod.Labels["middleware.alauda.io/announce_ip"], "-", ":") + announcePort := pod.Labels["middleware.alauda.io/announce_port"] + if announceIP != "" && announcePort != "" { + addr = net.JoinHostPort(announceIP, announcePort) + } + logger.Info("connect to redis", "addr", addr) + redisClient := redis.NewRedisClient(addr, *authInfo) + defer redisClient.Close() + + nodes, err := redisClient.Nodes(ctx) + if err != nil { + logger.Error(err, "get nodes info failed") + return err + } + currentNode := nodes.Self() + if !currentNode.IsJoined() { + logger.Info("unjoined node") + return fmt.Errorf("unjoined node") + } + if currentNode.Role == redis.MasterRole { + // this shard has got one new master + // clean and start + logger.Info("master nodes exists") + masterExists = true + return nil + } + + currentMaster := nodes.Get(currentNode.MasterId) + if currentMaster != nil && (currentMaster.Id != self.Id && + !strings.Contains(currentMaster.RawFlag, "fail") && + !strings.Contains(currentMaster.RawFlag, "noaddr")) { + masterExists = true + return nil + } + if err := doRedisFailover(ctx, redisClient, ForceFailoverAction, logger); err != nil { + return err + } + return nil + }(); err != nil { + continue + } + break + } + } + + if masterExists { + // check current rdb and aof + var ( + rdbFile = "/data/dump.rdb" + aofFile = "/data/appendonly.aof" + oldestModTime = time.Now().Add(time.Second * -3600) + ) + if info, _ := os.Stat(rdbFile); info != nil { + if info.ModTime().Before(oldestModTime) { + // clean this file + logger.Info("clean old dump.rdb") + os.Remove(rdbFile) + } + } + logger.Info("clean appendonly.aof") + os.Remove(aofFile) + } + return nil +} + +func generateRedisCluterNodeRecord(namespace, podName, shardId string) []byte { + tpl := `%s :0@0%s myself,master - 0 0 0 connected +vars currentEpoch 0 lastVoteEpoch 0` + if shardId != "" { + shardId = fmt.Sprintf(",,tls-port=0,shard-id=%s", shardId) + } + + nodeId := fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprintf("%s/%s", namespace, podName)))) // #nosec G401 + return []byte(fmt.Sprintf(tpl, nodeId, shardId)) +} + +func getPodsOfShard(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) (pods []corev1.Pod, err error) { + var ( + namespace = c.String("namespace") + podName = c.String("pod-name") + ) + if podName == "" { + podName, _ = os.Hostname() + } + if podName == "" { + return nil, nil + } + splitIndex := strings.LastIndex(podName, "-") + stsName := podName[0:splitIndex] + + labels := map[string]string{ + "middleware.instance/type": "distributed-redis-cluster", + "statefulSet": stsName, + } + + if err = commands.RetryGet(func() error { + if resp, err := client.CoreV1().Pods(namespace).List(ctx, v1.ListOptions{ + LabelSelector: v1.FormatLabelSelector(&v1.LabelSelector{MatchLabels: labels}), + Limit: 5, + }); err != nil { + return err + } else { + pods = resp.Items + } + return nil + }, 3); err != nil { + logger.Error(err, "list statefulset pods failed") + return nil, err + } + return +} + +type FailoverAction string + +const ( + NoFailoverAction FailoverAction = "" + ForceFailoverAction FailoverAction = "FORCE" +) + +func doRedisFailover(ctx context.Context, cli redis.RedisClient, action FailoverAction, logger logr.Logger) (err error) { + args := []interface{}{"FAILOVER"} + if action != "" { + args = append(args, action) + } + if _, err := cli.Do(ctx, "CLUSTER", args...); err != nil { + logger.Error(err, "do failover failed", "action", action) + return err + } + + for i := 0; i < 3; i++ { + logger.Info("check failover in 5s") + time.Sleep(time.Second * 5) + + nodes, err := cli.Nodes(ctx) + if err != nil { + logger.Error(err, "fetch cluster nodes failed") + return err + } + self := nodes.Self() + if self == nil { + return fmt.Errorf("get nodes info failed, as if the nodes.conf is broken") + } + if self.Role == redis.MasterRole { + logger.Info("failover succeed") + return nil + } + } + return fmt.Errorf("do manual failover failed") +} + +func getConfigMapData(ctx context.Context, client *kubernetes.Clientset, namespace, name, target string, logger logr.Logger) ([]byte, error) { + var cm *corev1.ConfigMap + if err := commands.RetryGet(func() (err error) { + cm, err = client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + return + }, 20); errors.IsNotFound(err) { + logger.Info("no synced nodes.conf found") + return nil, nil + } else if err != nil { + logger.Error(err, "get configmap failed", "name", name) + return nil, nil + } + return []byte(cm.Data[target]), nil +} diff --git a/cmd/redis-tools/commands/cluster/heal_test.go b/cmd/redis-tools/commands/cluster/heal_test.go new file mode 100644 index 0000000..8a4f9ad --- /dev/null +++ b/cmd/redis-tools/commands/cluster/heal_test.go @@ -0,0 +1,186 @@ +package cluster + +import ( + "context" + "reflect" + "sort" + "strings" + "testing" + + "github.com/go-logr/logr" +) + +func Test_portClusterNodesConf(t *testing.T) { + type args struct { + ctx context.Context + data []byte + logger logr.Logger + opts *HealOptions + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "new init node", + args: args{ + ctx: context.Background(), + data: []byte(`267600f4b192a940a20759aa0ebeee22f41d69e6 :0@0,shard-id=8a6d146963271fcd409b36704c86650827ef73a1 myself,master - 0 0 0 connected +vars currentEpoch 0 lastVoteEpoch 0`), + logger: logr.Discard(), + opts: &HealOptions{ + Namespace: "default", + PodName: "drc-c77-0-0", + Workspace: "/data", + TargetName: "nodes.conf", + Prefix: "sync-", + ShardID: "8a6d146963271fcd409b36704c86650827ef73a1", + NodeFile: "/data/nodes.conf", + }, + }, + want: []byte(`267600f4b192a940a20759aa0ebeee22f41d69e6 :0@0,shard-id=8a6d146963271fcd409b36704c86650827ef73a1 myself,master - 0 0 0 connected +vars currentEpoch 0 lastVoteEpoch 0`), + wantErr: false, + }, + { + name: "redis 6 nodes", + args: args{ + ctx: context.Background(), + data: []byte(`f89575a0d78cdc25b5ea1886bb88f1d979026c7d 192.168.132.183:32295@31500 slave c8b765997335f66f892ca6840f7f0b6df8200638 0 1709546093510 1 connected +c8b765997335f66f892ca6840f7f0b6df8200638 192.168.132.208:30471@30566 master - 0 1709546095505 1 connected 10923-16383 +4cc7fd15a841f081f8c956b0432f75baa170ea97 192.168.132.208:30969@30670 slave a8fa11eb3c3cc0c8115b8fe191d1e8ce92b857a5 0 1709546094000 2 connected +e8cd1219f9d712f7a1002962625f4f6ab46e4a69 192.168.132.209:31741@30771 myself,master - 0 1709546091000 3 connected 0-5461 +c4db03ea65954e1c2ced6135b8622360b5bf6ca7 192.168.132.183:31176@32087 slave e8cd1219f9d712f7a1002962625f4f6ab46e4a69 0 1709546092000 3 connected +a8fa11eb3c3cc0c8115b8fe191d1e8ce92b857a5 192.168.132.183:30550@31597 master - 0 1709546094511 2 connected 5462-10922 +vars currentEpoch 5 lastVoteEpoch 0`), + logger: logr.Discard(), + opts: &HealOptions{ + Namespace: "default", + PodName: "drc-c6-0-0", + Workspace: "/data", + TargetName: "nodes.conf", + Prefix: "sync-", + ShardID: "453e29079a2d30ac8122692ac4a1d1c9390acadf", + NodeFile: "/data/nodes.conf", + }, + }, + want: []byte(`f89575a0d78cdc25b5ea1886bb88f1d979026c7d 192.168.132.183:32295@31500 slave c8b765997335f66f892ca6840f7f0b6df8200638 0 1709546093510 1 connected +c8b765997335f66f892ca6840f7f0b6df8200638 192.168.132.208:30471@30566 master - 0 1709546095505 1 connected 10923-16383 +4cc7fd15a841f081f8c956b0432f75baa170ea97 192.168.132.208:30969@30670 slave a8fa11eb3c3cc0c8115b8fe191d1e8ce92b857a5 0 1709546094000 2 connected +e8cd1219f9d712f7a1002962625f4f6ab46e4a69 192.168.132.209:31741@30771,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf myself,master - 0 1709546091000 3 connected 0-5461 +c4db03ea65954e1c2ced6135b8622360b5bf6ca7 192.168.132.183:31176@32087,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf slave e8cd1219f9d712f7a1002962625f4f6ab46e4a69 0 1709546092000 3 connected +a8fa11eb3c3cc0c8115b8fe191d1e8ce92b857a5 192.168.132.183:30550@31597 master - 0 1709546094511 2 connected 5462-10922 +vars currentEpoch 5 lastVoteEpoch 0`), + wantErr: false, + }, + { + name: "fix redis 7 nodes", + args: args{ + ctx: context.Background(), + data: []byte(`14c4e058a702f5f3d8f1cd8b70cc3dd450f531ec 192.168.132.210:30541@32045,,tls-port=0,shard-id=78524c978001da077a73c07e1bde14f0eea5eb7d slave be0453515af6a6b9f6dbf96643604cbfc9517792 0 1709542562827 1 connected +c426f0d5a3986d926497f3e925887e4db9ea4e12 192.168.132.183:32304@31460,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 slave 41631ee411f0c6b1c25742f867ebbb1a63856772 0 1709542563825 2 connected +a1127158a63694800ac4e1187cc9fec7cae95dda 192.168.132.209:32287@31795,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 slave cce647d1c72bcac30aee07128c3aa1493405630e 0 1709542563000 0 connected +cce647d1c72bcac30aee07128c3aa1493405630e 192.168.132.208:31513@30183,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 master - 0 1709542561000 0 connected 5462-10922 +41631ee411f0c6b1c25742f867ebbb1a63856772 192.168.132.209:30707@30244,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 master - 0 1709542563000 2 connected 10923-16383 +be0453515af6a6b9f6dbf96643604cbfc9517792 192.168.132.183:31200@32418,,tls-port=0,shard-id=78524c978001da077a73c07e1bde14f0eea5eb7d myself,master - 0 1709542561000 1 connected 0-5461 +vars currentEpoch 5 lastVoteEpoch 0`), + logger: logr.Discard(), + opts: &HealOptions{ + Namespace: "default", + PodName: "drc-c7-0-0", + Workspace: "/data", + TargetName: "nodes.conf", + Prefix: "sync-", + ShardID: "453e29079a2d30ac8122692ac4a1d1c9390acadf", + NodeFile: "/data/nodes.conf", + }, + }, + want: []byte(`14c4e058a702f5f3d8f1cd8b70cc3dd450f531ec 192.168.132.210:30541@32045,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf slave be0453515af6a6b9f6dbf96643604cbfc9517792 0 1709542562827 1 connected +c426f0d5a3986d926497f3e925887e4db9ea4e12 192.168.132.183:32304@31460,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 slave 41631ee411f0c6b1c25742f867ebbb1a63856772 0 1709542563825 2 connected +a1127158a63694800ac4e1187cc9fec7cae95dda 192.168.132.209:32287@31795,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 slave cce647d1c72bcac30aee07128c3aa1493405630e 0 1709542563000 0 connected +cce647d1c72bcac30aee07128c3aa1493405630e 192.168.132.208:31513@30183,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 master - 0 1709542561000 0 connected 5462-10922 +41631ee411f0c6b1c25742f867ebbb1a63856772 192.168.132.209:30707@30244,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 master - 0 1709542563000 2 connected 10923-16383 +be0453515af6a6b9f6dbf96643604cbfc9517792 192.168.132.183:31200@32418,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf myself,master - 0 1709542561000 1 connected 0-5461 +vars currentEpoch 5 lastVoteEpoch 0`), + wantErr: false, + }, + { + name: "fix redis 7 myself,slave", + args: args{ + ctx: context.Background(), + data: []byte(`14c4e058a702f5f3d8f1cd8b70cc3dd450f531ec 192.168.132.210:30541@32045,,tls-port=0,shard-id=78524c978001da077a73c07e1bde14f0eea5eb7d myself,slave be0453515af6a6b9f6dbf96643604cbfc9517792 0 1709542562827 1 connected +c426f0d5a3986d926497f3e925887e4db9ea4e12 192.168.132.183:32304@31460,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 slave 41631ee411f0c6b1c25742f867ebbb1a63856772 0 1709542563825 2 connected +a1127158a63694800ac4e1187cc9fec7cae95dda 192.168.132.209:32287@31795,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 slave cce647d1c72bcac30aee07128c3aa1493405630e 0 1709542563000 0 connected +cce647d1c72bcac30aee07128c3aa1493405630e 192.168.132.208:31513@30183,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 master - 0 1709542561000 0 connected 5462-10922 +41631ee411f0c6b1c25742f867ebbb1a63856772 192.168.132.209:30707@30244,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 master - 0 1709542563000 2 connected 10923-16383 +be0453515af6a6b9f6dbf96643604cbfc9517792 192.168.132.183:31200@32418,,tls-port=0,shard-id=78524c978001da077a73c07e1bde14f0eea5eb7d master - 0 1709542561000 1 connected 0-5461 +vars currentEpoch 5 lastVoteEpoch 0`), + logger: logr.Discard(), + opts: &HealOptions{ + Namespace: "default", + PodName: "drc-c7-0-0", + Workspace: "/data", + TargetName: "nodes.conf", + Prefix: "sync-", + ShardID: "453e29079a2d30ac8122692ac4a1d1c9390acadf", + NodeFile: "/data/nodes.conf", + }, + }, + want: []byte(`14c4e058a702f5f3d8f1cd8b70cc3dd450f531ec 192.168.132.210:30541@32045,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf myself,slave be0453515af6a6b9f6dbf96643604cbfc9517792 0 1709542562827 1 connected +c426f0d5a3986d926497f3e925887e4db9ea4e12 192.168.132.183:32304@31460,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 slave 41631ee411f0c6b1c25742f867ebbb1a63856772 0 1709542563825 2 connected +a1127158a63694800ac4e1187cc9fec7cae95dda 192.168.132.209:32287@31795,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 slave cce647d1c72bcac30aee07128c3aa1493405630e 0 1709542563000 0 connected +cce647d1c72bcac30aee07128c3aa1493405630e 192.168.132.208:31513@30183,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 master - 0 1709542561000 0 connected 5462-10922 +41631ee411f0c6b1c25742f867ebbb1a63856772 192.168.132.209:30707@30244,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 master - 0 1709542563000 2 connected 10923-16383 +be0453515af6a6b9f6dbf96643604cbfc9517792 192.168.132.183:31200@32418,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf master - 0 1709542561000 1 connected 0-5461 +vars currentEpoch 5 lastVoteEpoch 0`), + wantErr: false, + }, + { + name: "fix shard-id conflict with other shard", + args: args{ + ctx: context.Background(), + data: []byte(`14c4e058a702f5f3d8f1cd8b70cc3dd450f531ec 192.168.132.210:30541@32045,,tls-port=0,shard-id=78524c978001da077a73c07e1bde14f0eea5eb7d slave be0453515af6a6b9f6dbf96643604cbfc9517792 0 1709542562827 1 connected +c426f0d5a3986d926497f3e925887e4db9ea4e12 192.168.132.183:32304@31460,,tls-port=0,shard-id=da7d249987641fb2379cce47636469153c5e7e58 slave 41631ee411f0c6b1c25742f867ebbb1a63856772 0 1709542563825 2 connected +a1127158a63694800ac4e1187cc9fec7cae95dda 192.168.132.209:32287@31795,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 slave cce647d1c72bcac30aee07128c3aa1493405630e 0 1709542563000 0 connected +cce647d1c72bcac30aee07128c3aa1493405630e 192.168.132.208:31513@30183,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 master - 0 1709542561000 0 connected 5462-10922 +41631ee411f0c6b1c25742f867ebbb1a63856772 192.168.132.209:30707@30244,,tls-port=0,shard-id=1a7d249987641fb2379cce47636469153c5e7e58 master - 0 1709542563000 2 connected 10923-16383 +be0453515af6a6b9f6dbf96643604cbfc9517792 192.168.132.183:31200@32418,,tls-port=0,shard-id=78524c978001da077a73c07e1bde14f0eea5eb7d myself,master - 0 1709542561000 1 connected 0-5461 +vars currentEpoch 5 lastVoteEpoch 0`), + logger: logr.Discard(), + opts: &HealOptions{ + Namespace: "default", + PodName: "drc-c7-0-0", + Workspace: "/data", + TargetName: "nodes.conf", + Prefix: "sync-", + ShardID: "453e29079a2d30ac8122692ac4a1d1c9390acadf", + NodeFile: "/data/nodes.conf", + }, + }, + want: []byte(`14c4e058a702f5f3d8f1cd8b70cc3dd450f531ec 192.168.132.210:30541@32045,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf slave be0453515af6a6b9f6dbf96643604cbfc9517792 0 1709542562827 1 connected +a1127158a63694800ac4e1187cc9fec7cae95dda 192.168.132.209:32287@31795,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 slave cce647d1c72bcac30aee07128c3aa1493405630e 0 1709542563000 0 connected +cce647d1c72bcac30aee07128c3aa1493405630e 192.168.132.208:31513@30183,,tls-port=0,shard-id=a262f15901feff7a44f73a8d64210708909db3a5 master - 0 1709542561000 0 connected 5462-10922 +be0453515af6a6b9f6dbf96643604cbfc9517792 192.168.132.183:31200@32418,,tls-port=0,shard-id=453e29079a2d30ac8122692ac4a1d1c9390acadf myself,master - 0 1709542561000 1 connected 0-5461 +vars currentEpoch 5 lastVoteEpoch 0`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := portClusterNodesConf(tt.args.ctx, tt.args.data, tt.args.logger, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("portClusterNodesConf() error = %v, wantErr %v", err, tt.wantErr) + return + } + gotVals := strings.Split(string(got), "\n") + wantVals := strings.Split(string(tt.want), "\n") + sort.Strings(gotVals) + sort.Strings(wantVals) + if !reflect.DeepEqual(gotVals, wantVals) { + t.Errorf("portClusterNodesConf() = (%d)\n%s\n## want (%d)\n%s", len(got), strings.Join(gotVals, "\n"), len(tt.want), strings.Join(wantVals, "\n")) + } + }) + } +} diff --git a/cmd/redis-tools/pkg/commands/cluster/healthcheck.go b/cmd/redis-tools/commands/cluster/healthcheck.go similarity index 54% rename from cmd/redis-tools/pkg/commands/cluster/healthcheck.go rename to cmd/redis-tools/commands/cluster/healthcheck.go index 2905eb2..e69dfba 100644 --- a/cmd/redis-tools/pkg/commands/cluster/healthcheck.go +++ b/cmd/redis-tools/commands/cluster/healthcheck.go @@ -1,19 +1,3 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package cluster import ( @@ -22,11 +6,11 @@ import ( "net" "time" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" + "github.com/alauda/redis-operator/pkg/redis" ) func Readiness(ctx context.Context, addr string, authInfo redis.AuthInfo) error { - client := redis.NewClient(addr, authInfo) + client := redis.NewRedisClient(addr, authInfo) defer client.Close() nodes, err := client.Nodes(ctx) @@ -48,7 +32,7 @@ func Readiness(ctx context.Context, addr string, authInfo redis.AuthInfo) error // Ping func Ping(ctx context.Context, addr string, authInfo redis.AuthInfo) error { - client := redis.NewClient(addr, authInfo) + client := redis.NewRedisClient(addr, authInfo) defer client.Close() if _, err := client.Do(ctx, "PING"); err != nil { diff --git a/cmd/redis-tools/commands/cluster/shutdown.go b/cmd/redis-tools/commands/cluster/shutdown.go new file mode 100644 index 0000000..1c16727 --- /dev/null +++ b/cmd/redis-tools/commands/cluster/shutdown.go @@ -0,0 +1,209 @@ +package cluster + +import ( + "context" + "errors" + "fmt" + "io" + "math/rand" + "net" + "net/netip" + "os" + "time" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/cmd/redis-tools/commands/runner" + "github.com/alauda/redis-operator/pkg/redis" + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" +) + +// Shutdown 在退出时做 failover +// +// NOTE: 在4.0, 5.0中,在更新密码时,会重启实例。但是由于密码在重启前已经热更新,导致其他脚本无法连接到实例,包括shutdown脚本 +// 为了解决这个问题,针对4,5 版本,会在重启前,先做failover,将master failover 到-0 节点。 +// 由于重启是逆序的,最后一个pod启动成功之后,会使用新密码连接到 master,从而确保服务一直可用,切数据不会丢失 +func Shutdown(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { + var ( + podName = c.String("pod-name") + timeout = time.Duration(c.Int("timeout")) * time.Second + ) + if timeout == 0 { + timeout = time.Second * 300 + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + logger.Info("check local nodes.conf") + authInfo, err := commands.LoadAuthInfo(c, ctx) + if err != nil { + logger.Error(err, "load redis operator user info failed") + return err + } + + addr := net.JoinHostPort("local.inject", "6379") + redisClient := redis.NewRedisClient(addr, *authInfo) + defer redisClient.Close() + + logger.Info("rewrite nodes.conf") + if _, err := redisClient.Do(ctx, "CLUSTER", "SAVECONFIG"); err != nil { + logger.Error(err, "rewrite nodes.conf failed, ignored") + } + + // sync current nodes.conf to configmap + logger.Info("persistent nodes.conf to configmap") + if err := runner.SyncFromLocalToEtcd(c, ctx, "", false, logger); err != nil { + logger.Error(err, "persistent nodes.conf to configmap failed") + } + + // get all nodes + data, err := redis.Bytes(redisClient.Do(ctx, "CLUSTER", "NODES")) + if err != nil { + logger.Error(err, "get cluster nodes failed") + return nil + } + + nodes, err := redis.ParseNodes(string(data)) + if err != nil { + logger.Error(err, "parse cluster nodes failed") + return nil + } + if nodes == nil { + logger.Info("no nodes found") + return nil + } + + self := nodes.Self() + if !self.IsJoined() { + logger.Info("node not joined") + return nil + } + + // NOTE: disable auto failover for terminating pods + // TODO: update this config to cluster-replica-no-failover + configName := "cluster-slave-no-failover" + if _, err := redisClient.Do(ctx, "CONFIG", "SET", configName, "yes"); err != nil { + logger.Error(err, "disable slave failover failed") + } + + if self.Role == redis.MasterRole { + + getCandidatePod := func() (*v1.Pod, error) { + // find pod which is a replica of me + pods, err := getPodsOfShard(ctx, c, client, logger) + if err != nil { + logger.Error(err, "list pods failed") + return nil, err + } + if len(pods) == 1 { + return nil, nil + } + for _, pod := range pods { + if pod.GetName() == podName { + continue + } + if pod.GetDeletionTimestamp() != nil { + continue + } + + if !func() bool { + for _, cont := range pod.Status.ContainerStatuses { + if cont.Name == "redis" && cont.Ready { + return true + } + } + return false + }() { + continue + } + + addr := getPodAccessAddr(pod.DeepCopy()) + logger.Info("check node", "pod", pod.GetName(), "addr", addr) + redisClient := redis.NewRedisClient(addr, *authInfo) + defer redisClient.Close() + + nodes, err := redisClient.Nodes(ctx) + if err != nil { + logger.Error(err, "load cluster nodes failed") + return nil, err + } + currentNode := nodes.Self() + if currentNode.MasterId == self.Id { + return pod.DeepCopy(), nil + } + } + return nil, fmt.Errorf("not candidate pod found") + } + + for i := 0; i < 20; i++ { + logger.Info(fmt.Sprintf("try %d failover", i)) + if err := func() error { + canPod, err := getCandidatePod() + if err != nil { + logger.Error(err, "get candidate pod failed") + return err + } else if canPod == nil { + return nil + } + + randInt := rand.Intn(50) + 1 // #nosec: ignore + duration := time.Duration(randInt) * time.Second + logger.Info(fmt.Sprintf("Wait for %s to escape failover conflict", duration)) + time.Sleep(duration) + + addr := getPodAccessAddr(canPod) + redisClient := redis.NewRedisClient(addr, *authInfo) + defer redisClient.Close() + + nodes, err := redisClient.Nodes(ctx) + if err != nil { + logger.Error(err, "load cluster nodes failed") + return err + } + mastrNode := nodes.Get(self.Id) + action := NoFailoverAction + if mastrNode.IsFailed() { + action = ForceFailoverAction + } + if err := doRedisFailover(ctx, redisClient, action, logger); err != nil { + logger.Error(err, "do failed failed") + return err + } + return nil + }(); err == nil { + break + } + time.Sleep(time.Second * 5) + } + } + + // wait for some time for nodes to sync info + time.Sleep(time.Second * 10) + + logger.Info("do shutdown node") + if _, err = redisClient.Do(ctx, "SHUTDOWN"); err != nil && !errors.Is(err, io.EOF) { + logger.Error(err, "graceful shutdown failed") + } + return nil +} + +func getPodAccessAddr(pod *v1.Pod) string { + addr := net.JoinHostPort(pod.Status.PodIP, "6379") + ipFamilyPrefer := os.Getenv("IP_FAMILY_PREFER") + if ipFamilyPrefer != "" { + for _, podIp := range pod.Status.PodIPs { + ip, _ := netip.ParseAddr(podIp.IP) + if ip.Is6() && ipFamilyPrefer == string(v1.IPv6Protocol) { + addr = net.JoinHostPort(podIp.IP, "6379") + break + } else if ip.Is4() && ipFamilyPrefer == string(v1.IPv4Protocol) { + addr = net.JoinHostPort(podIp.IP, "6379") + break + } + } + } + return addr +} diff --git a/cmd/redis-tools/commands/failover/access.go b/cmd/redis-tools/commands/failover/access.go new file mode 100644 index 0000000..bb29fda --- /dev/null +++ b/cmd/redis-tools/commands/failover/access.go @@ -0,0 +1,164 @@ +package failover + +import ( + "context" + "fmt" + "net/netip" + "os" + "strconv" + "strings" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" +) + +func Access(ctx context.Context, client *kubernetes.Clientset, namespace, podName, ipfamily string, serviceType corev1.ServiceType, logger logr.Logger) error { + logger.Info("service access", "serviceType", serviceType, "ipfamily", ipfamily, "podName", podName) + pod, err := commands.GetPod(ctx, client, namespace, podName, logger) + if err != nil { + logger.Error(err, "get pods failed", "namespace", namespace, "name", podName) + return err + } + + if pod.Status.HostIP == "" { + return fmt.Errorf("pod not found or pod with invalid hostIP") + } + + var ( + announceIp = pod.Status.PodIP + announcePort int32 = 6379 + ) + if serviceType == corev1.ServiceTypeNodePort { + podSvc, err := commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeNodePort, 20, logger) + if errors.IsNotFound(err) { + if podSvc, err = commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeNodePort, 20, logger); err != nil { + logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + } else if err != nil { + logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + for _, v := range podSvc.Spec.Ports { + if v.Name == "client" { + announcePort = v.NodePort + } + } + + node, err := client.CoreV1().Nodes().Get(ctx, pod.Spec.NodeName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "get nodes err", "node", node.Name) + return err + } + logger.Info("get nodes success", "Name", node.Name) + + var addresses []string + for _, addr := range node.Status.Addresses { + if addr.Address == "" { + continue + } + + switch addr.Type { + case corev1.NodeExternalIP: + ip, err := netip.ParseAddr(addr.Address) + if err != nil { + logger.Error(err, "parse address err", "address", addr.Address) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + addresses = append(addresses, addr.Address) + } else if ipfamily != "IPv6" && ip.Is4() { + addresses = append(addresses, addr.Address) + } + case corev1.NodeInternalIP: + // internal ip first + ip, err := netip.ParseAddr(addr.Address) + if err != nil { + logger.Error(err, "parse address err", "address", addr.Address) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + addresses = append([]string{addr.Address}, addresses...) + addresses = append(addresses, addr.Address) + } else if ipfamily != "IPv6" && ip.Is4() { + addresses = append([]string{addr.Address}, addresses...) + } + } + } + if len(addresses) > 0 { + announceIp = addresses[0] + } else { + err := fmt.Errorf("no available address") + logger.Error(err, "get usable address failed") + return err + } + } else if serviceType == corev1.ServiceTypeLoadBalancer { + podSvc, err := commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeLoadBalancer, 20, logger) + if errors.IsNotFound(err) { + if podSvc, err = commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeLoadBalancer, 20, logger); err != nil { + logger.Error(err, "retry get lb service failed") + return err + } + } else if err != nil { + logger.Error(err, "get lb service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + + for _, v := range podSvc.Status.LoadBalancer.Ingress { + if v.IP == "" { + continue + } + + ip, err := netip.ParseAddr(v.IP) + if err != nil { + logger.Error(err, "parse address err", "address", v.IP) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + announceIp = v.IP + break + } + if ipfamily != "IPv6" && ip.Is4() { + announceIp = v.IP + break + } + } + } else { + for _, addr := range pod.Status.PodIPs { + ip, err := netip.ParseAddr(addr.IP) + if err != nil { + return err + } + if ipfamily == "IPv6" && ip.Is6() { + announceIp = addr.IP + break + } else if ipfamily != "IPv6" && ip.Is4() { + announceIp = addr.IP + break + } + } + } + + format_announceIp := strings.Replace(announceIp, ":", "-", -1) + labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s"},{"op":"add","path":"/metadata/labels/%s","value":"%s"}]`, + strings.Replace("middleware.alauda.io/announce_ip", "/", "~1", -1), format_announceIp, + strings.Replace("middleware.alauda.io/announce_port", "/", "~1", -1), strconv.Itoa(int(announcePort))) + + logger.Info(labelPatch) + _, err = client.CoreV1().Pods(pod.Namespace).Patch(ctx, podName, types.JSONPatchType, []byte(labelPatch), metav1.PatchOptions{}) + if err != nil { + logger.Error(err, "patch pod label failed") + return err + } + sentinelConfigContent := fmt.Sprintf(` +slave-announce-ip %s +slave-announce-port %d +`, announceIp, announcePort) + + return os.WriteFile("/data/announce.conf", []byte(sentinelConfigContent), 0644) // #nosec G306 +} diff --git a/cmd/redis-tools/commands/failover/command.go b/cmd/redis-tools/commands/failover/command.go new file mode 100644 index 0000000..a4ba988 --- /dev/null +++ b/cmd/redis-tools/commands/failover/command.go @@ -0,0 +1,183 @@ +package failover + +import ( + "context" + + "github.com/alauda/redis-operator/cmd/redis-tools/util" + "github.com/urfave/cli/v2" + corev1 "k8s.io/api/core/v1" +) + +func NewCommand(ctx context.Context) *cli.Command { + return &cli.Command{ + Name: "failover", + Usage: "Failover set commands", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Usage: "Namespace of current pod", + EnvVars: []string{"NAMESPACE"}, + }, + &cli.StringFlag{ + Name: "pod-name", + Usage: "The name of current pod", + EnvVars: []string{"POD_NAME"}, + }, + &cli.StringFlag{ + Name: "pod-ips", + Usage: "The ips of current pod", + EnvVars: []string{"POD_IPS"}, + }, + &cli.StringFlag{ + Name: "pod-uid", + Usage: "The id of current pod", + EnvVars: []string{"POD_UID"}, + }, + &cli.StringFlag{ + Name: "service-name", + Usage: "Service name of the statefulset", + EnvVars: []string{"SERVICE_NAME"}, + }, + &cli.StringFlag{ + Name: "operator-username", + Usage: "Operator username", + EnvVars: []string{"OPERATOR_USERNAME"}, + }, + &cli.StringFlag{ + Name: "operator-secret-name", + Usage: "Operator user password secret name", + EnvVars: []string{"OPERATOR_SECRET_NAME"}, + }, + &cli.BoolFlag{ + Name: "acl", + Usage: "Enable acl", + EnvVars: []string{"ACL_ENABLED"}, + Hidden: true, + }, + &cli.StringFlag{ + Name: "acl-config", + Usage: "Acl config map name", + EnvVars: []string{"ACL_CONFIGMAP_NAME"}, + }, + &cli.BoolFlag{ + Name: "tls", + Usage: "Enable tls", + EnvVars: []string{"TLS_ENABLED"}, + }, + &cli.StringFlag{ + Name: "tls-key-file", + Usage: "Name of the client key file (including full path)", + EnvVars: []string{"TLS_CLIENT_KEY_FILE"}, + Value: "/tls/tls.key", + }, + &cli.StringFlag{ + Name: "tls-cert-file", + Usage: "Name of the client certificate file (including full path)", + EnvVars: []string{"TLS_CLIENT_CERT_FILE"}, + Value: "/tls/tls.crt", + }, + &cli.StringFlag{ + Name: "tls-ca-file", + Usage: "Name of the ca file (including full path)", + EnvVars: []string{"TLS_CA_CERT_FILE"}, + Value: "/tls/ca.crt", + }, + &cli.StringFlag{ + Name: "nodeport-enabled", + Usage: "nodeport switch", + EnvVars: []string{"NODEPORT_ENABLED"}, + }, + &cli.StringFlag{ + Name: "ip-family", + Usage: "IP_FAMILY for servie access", + EnvVars: []string{"IP_FAMILY_PREFER"}, + }, + &cli.StringFlag{ + Name: "service-type", + Usage: "Service type for sentinel service", + EnvVars: []string{"SERVICE_TYPE"}, + Value: "ClusterIP", + }, + }, + Subcommands: []*cli.Command{ + { + Name: "expose", + Usage: "Create nodeport service for current pod to announce", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) error { + var ( + namespace = c.String("namespace") + podName = c.String("pod-name") + ipFamily = c.String("ip-family") + serviceType = corev1.ServiceType(c.String("service-type")) + ) + if namespace == "" { + return cli.Exit("require namespace", 1) + } + if podName == "" { + return cli.Exit("require podname", 1) + } + + logger := util.NewLogger(c).WithName("Access") + + client, err := util.NewClient() + if err != nil { + logger.Error(err, "create k8s client failed, error=%s", err) + return cli.Exit(err, 1) + } + + if err := Access(ctx, client, namespace, podName, ipFamily, serviceType, logger); err != nil { + logger.Error(err, "enable nodeport service access failed") + return cli.Exit(err, 1) + } + return nil + }, + }, + { + Name: "shutdown", + Usage: "Shutdown redis nodes", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "monitor-policy", + Usage: "Monitor policy for redis replication", + EnvVars: []string{"MONITOR_POLICY"}, + }, + &cli.StringFlag{ + Name: "monitor-uri", + Usage: "Monitor uri for failover", + EnvVars: []string{"MONITOR_URI"}, + }, + &cli.StringFlag{ + Name: "monitor-operator-secret-name", + Usage: "Monitor operator secret name", + EnvVars: []string{"MONITOR_OPERATOR_SECRET_NAME"}, + }, + &cli.StringFlag{ + Name: "name", + Usage: "Monitor name", + Value: "mymaster", + }, + &cli.IntFlag{ + Name: "timeout", + Aliases: []string{"t"}, + Usage: "Timeout time of shutdown", + Value: 300, + }, + }, + Action: func(c *cli.Context) error { + logger := util.NewLogger(c).WithName("Shutdown") + + client, err := util.NewClient() + if err != nil { + logger.Error(err, "create k8s client failed, error=%s", err) + return cli.Exit(err, 1) + } + if err := Shutdown(ctx, c, client, logger); err != nil { + return cli.Exit(err, 1) + } + return nil + }, + }, + }, + } +} diff --git a/cmd/redis-tools/commands/failover/shutdown.go b/cmd/redis-tools/commands/failover/shutdown.go new file mode 100644 index 0000000..bcd00b2 --- /dev/null +++ b/cmd/redis-tools/commands/failover/shutdown.go @@ -0,0 +1,200 @@ +package failover + +import ( + "context" + "errors" + "io" + "net" + "net/url" + "strings" + "time" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/pkg/redis" + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + "k8s.io/client-go/kubernetes" +) + +// Shutdown 在退出时做 failover +// +// NOTE: 在4.0, 5.0中,在更新密码时,会重启实例。但是由于密码在重启前已经热更新,导致其他脚本无法连接到实例,包括shutdown脚本 +// 为了解决这个问题,针对4,5 版本,会在重启前,先做failover,将master failover 到-0 节点。 +// 由于重启是逆序的,最后一个pod启动成功之后,会使用新密码连接到 master,从而确保服务一直可用,切数据不会丢失 +func Shutdown(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { + var ( + podIPs = c.String("pod-ips") + name = c.String("name") + monitor = strings.ToLower(c.String("monitor-policy")) + timeout = time.Duration(c.Int("timeout")) * time.Second + ) + if timeout == 0 { + timeout = time.Second * 300 + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + authInfo, err := commands.LoadAuthInfo(c, ctx) + if err != nil { + logger.Error(err, "load redis operator user info failed") + return err + } + senAuthInfo, err := commands.LoadMonitorAuthInfo(c, ctx, client) + if err != nil { + logger.Error(err, "load redis operator user info failed") + return err + } + + addr := net.JoinHostPort("local.inject", "6379") + redisClient := redis.NewRedisClient(addr, *authInfo) + defer redisClient.Close() + + info, err := func() (*redis.RedisInfo, error) { + var ( + err error + info *redis.RedisInfo + ) + for i := 0; i < 5; i++ { + if info, err = redisClient.Info(ctx); err != nil { + logger.Error(err, "get info failed, retry...") + time.Sleep(time.Second) + continue + } + break + } + return info, err + }() + if err != nil { + logger.Error(err, "check node role failed, abort auto failover") + return err + } + + if info.Role == "master" && monitor == "sentinel" { + err = func() error { + serveAddresses := map[string]struct{}{} + if podIPs != "" { + for _, ip := range strings.Split(podIPs, ",") { + serveAddresses[net.JoinHostPort(ip, "6379")] = struct{}{} + } + } + if config, err := func() (map[string]string, error) { + var ( + err error + cfg map[string]string + ) + for i := 0; i < 5; i++ { + cfg, err = redisClient.ConfigGet(ctx, "*") + if err != nil { + logger.Error(err, "get config failed") + time.Sleep(time.Second) + continue + } + break + } + return cfg, err + }(); err != nil { + logger.Error(err, "get config failed ") + } else { + ip, port := config["replica-announce-ip"], config["replica-announce-port"] + if ip != "" && port != "" { + serveAddresses[net.JoinHostPort(ip, port)] = struct{}{} + } + } + + var sentinelNodes []string + if val := c.String("monitor-uri"); val == "" { + logger.Error(err, "require monitor uri") + return errors.New("require monitor uri") + } else if u, err := url.Parse(val); err != nil { + logger.Error(err, "parse monitor uri failed") + return err + } else { + sentinelNodes = strings.Split(u.Host, ",") + } + + var senClient redis.RedisClient + for _, node := range sentinelNodes { + if senClient, err = func() (redis.RedisClient, error) { + senClient := redis.NewRedisClient(node, *senAuthInfo) + if _, err := senClient.DoWithTimeout(ctx, time.Second, "PING"); err != nil { + logger.Error(err, "ping node failed") + senClient.Close() + return nil, err + } + return senClient, nil + }(); senClient != nil { + break + } + } + if senClient == nil { + logger.Error(err, "get sentinel client failed") + return err + } + defer senClient.Close() + + __FAILOVER_END__: + for i := 0; ; i++ { + info, err := senClient.Info(ctx) + if err != nil { + if strings.Contains(err.Error(), "no such host") { + logger.Error(err, "sentinel node is down, give up manual failover") + break __FAILOVER_END__ + } + logger.Error(err, "get info failed") + time.Sleep(time.Second) + continue + } + addr := info.SentinelMaster0.Address.String() + if _, ok := serveAddresses[addr]; !ok { + logger.Info("current node is not master, skip failover", "master", addr) + break __FAILOVER_END__ + } + if info.SentinelMaster0.Replicas == 0 { + logger.Info("current master has no replicas, skip failover") + break __FAILOVER_END__ + } + + logger.Info("current node is master, try failover", "master", addr) + if _, err = senClient.DoWithTimeout(ctx, time.Second, "SENTINEL", "FAILOVER", name); err != nil { + if strings.Contains(err.Error(), "no such host") { + logger.Error(err, "sentinel node is down, give up manual failover") + break __FAILOVER_END__ + } else if strings.Contains(err.Error(), "INPROG Failover already in progress") { + logger.Info("failover in progress") + } else { + logger.Error(err, "do failover failed, retry in 10s") + time.Sleep(time.Second * 10) + continue + } + } + for j := 0; j < 6; j++ { + if info, err = senClient.Info(ctx); err != nil { + if strings.Contains(err.Error(), "no such host") { + logger.Error(err, "sentinel node is down, give up manual failover") + break __FAILOVER_END__ + } + logger.Error(err, "get info failed") + } else { + addr := info.SentinelMaster0.Address.String() + if _, ok := serveAddresses[addr]; !ok { + logger.Info("failover success", "master", addr) + break __FAILOVER_END__ + } + } + time.Sleep(time.Second * 5) + } + } + return nil + }() + } + + time.Sleep(time.Second * 10) + + logger.Info("do shutdown node") + // NOTE: here set timeout to 300s, which will try best to do a shutdown snapshot + // if the data is too large, this snapshot may not be completed + if _, err := redisClient.DoWithTimeout(ctx, time.Second*300, "SHUTDOWN"); err != nil && !errors.Is(err, io.EOF) { + logger.Error(err, "graceful shutdown failed") + } + return err +} diff --git a/cmd/redis-tools/commands/helper.go b/cmd/redis-tools/commands/helper.go new file mode 100644 index 0000000..92040be --- /dev/null +++ b/cmd/redis-tools/commands/helper.go @@ -0,0 +1,228 @@ +package commands + +import ( + "context" + "crypto/tls" + "fmt" + "os" + "time" + + "github.com/alauda/redis-operator/pkg/redis" + + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/retry" +) + +const ( + DefaultSecretMountPath = "/account/password" // #nosec G101 + InjectedPasswordPath = "/tmp/newpass" // #nosec G101 +) + +func LoadMonitorAuthInfo(c *cli.Context, ctx context.Context, client *kubernetes.Clientset) (*redis.AuthInfo, error) { + var ( + namespace = c.String("namespace") + passwordSecret = c.String("monitor-operator-secret-name") + // tls + isTLSEnabled = c.Bool("tls") + tlsKeyFile = c.String("tls-key-file") + tlsCertFile = c.String("tls-cert-file") + ) + + var ( + err error + tlsConf *tls.Config + password string + ) + if passwordSecret != "" { + if err := RetryGet(func() error { + if data, err := client.CoreV1().Secrets(namespace).Get(ctx, passwordSecret, metav1.GetOptions{}); err != nil { + return err + } else { + password = string(data.Data["password"]) + } + return nil + }); err != nil { + return nil, err + } + } + if isTLSEnabled { + if tlsConf, err = LoadTLSCofig(tlsKeyFile, tlsCertFile); err != nil { + return nil, err + } + } + return &redis.AuthInfo{ + Password: password, + TLSConfig: tlsConf, + }, nil +} + +func LoadAuthInfo(c *cli.Context, ctx context.Context) (*redis.AuthInfo, error) { + var ( + // acl + opUsername = c.String("operator-username") + // tls + isTLSEnabled = c.Bool("tls") + tlsKeyFile = c.String("tls-key-file") + tlsCertFile = c.String("tls-cert-file") + ) + + var ( + err error + tlsConf *tls.Config + password string + passwordPath = DefaultSecretMountPath + ) + if opUsername == "" || opUsername == "default" { + if _, err := os.Stat(InjectedPasswordPath); err == nil { + passwordPath = InjectedPasswordPath + } + } + if data, err := os.ReadFile(passwordPath); err != nil && !os.IsNotExist(err) { + return nil, err + } else { + password = string(data) + } + + if isTLSEnabled { + if tlsConf, err = LoadTLSCofig(tlsKeyFile, tlsCertFile); err != nil { + return nil, err + } + } + return &redis.AuthInfo{ + Username: opUsername, + Password: password, + TLSConfig: tlsConf, + }, nil +} + +func LoadTLSCofig(tlsKeyFile, tlsCertFile string) (*tls.Config, error) { + if tlsKeyFile == "" || tlsCertFile == "" { + return nil, fmt.Errorf("tls file path not configed") + } + cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) + if err != nil { + return nil, err + } + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, // #nosec G402 + }, nil +} + +// NewOwnerReference +func NewOwnerReference(ctx context.Context, client *kubernetes.Clientset, namespace, podName string) ([]metav1.OwnerReference, error) { + if client == nil { + return nil, nil + } + + pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + var name string + for _, ownerRef := range pod.OwnerReferences { + if ownerRef.Kind == "StatefulSet" { + name = ownerRef.Name + break + } + } + if sts, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}); err != nil { + return nil, err + } else { + return sts.OwnerReferences, nil + } +} + +func RetryGet(f func() error, steps ...int) error { + step := 5 + if len(steps) > 0 && steps[0] > 0 { + step = steps[0] + } + return retry.OnError(wait.Backoff{ + Steps: step, + Duration: 400 * time.Millisecond, + Factor: 2.0, + Jitter: 2, + }, func(err error) bool { + return errors.IsInternalError(err) || errors.IsServerTimeout(err) || errors.IsServiceUnavailable(err) || + errors.IsTimeout(err) || errors.IsTooManyRequests(err) + }, f) +} + +func GetPod(ctx context.Context, client *kubernetes.Clientset, namespace, name string, logger logr.Logger) (*corev1.Pod, error) { + var pod *corev1.Pod + if err := RetryGet(func() (err error) { + logger.Info("get pods ip", "namespace", namespace, "name", name) + if pod, err = client.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}); err != nil { + logger.Error(err, "get pods failed") + return err + } else if pod.Status.PodIP == "" { + return errors.NewTimeoutError("pod have not assigied pod ip", 0) + } else if pod.Status.HostIP == "" { + return errors.NewTimeoutError("pod have not assigied host ip", 0) + } + return + }, 20); err != nil { + return nil, err + } + return pod, nil +} + +func RetryGetService(ctx context.Context, clientset *kubernetes.Clientset, svcNamespace, svcName string, typ corev1.ServiceType, + count int, logger logr.Logger) (*corev1.Service, error) { + + serviceChecker := func(svc *corev1.Service, typ corev1.ServiceType) error { + if svc == nil { + return fmt.Errorf("service not found") + } + if len(svc.Spec.Ports) < 1 { + return fmt.Errorf("service port not found") + } + + if svc.Spec.Type != typ { + return fmt.Errorf("service type not match") + } + + switch svc.Spec.Type { + case corev1.ServiceTypeNodePort: + for _, port := range svc.Spec.Ports { + if port.NodePort == 0 { + return fmt.Errorf("service nodeport not found for port %d", port.Port) + } + } + case corev1.ServiceTypeLoadBalancer: + if len(svc.Status.LoadBalancer.Ingress) < 1 { + return fmt.Errorf("service loadbalancer ip not found") + } else { + for _, v := range svc.Status.LoadBalancer.Ingress { + if v.IP == "" { + return fmt.Errorf("service loadbalancer ip is empty") + } + } + } + } + return nil + } + + logger.Info("retry get service", "target", fmt.Sprintf("%s/%s", svcNamespace, svcName), "count", count) + for i := 0; i < count+1; i++ { + svc, err := clientset.CoreV1().Services(svcNamespace).Get(ctx, svcName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", svcNamespace, svcName)) + return nil, err + } + if serviceChecker(svc, typ) != nil { + logger.Error(err, "service check failed", "target", fmt.Sprintf("%s/%s", svcNamespace, svcName)) + } else { + return svc, nil + } + time.Sleep(time.Second * 3) + } + return nil, fmt.Errorf("service %s/%s not ready", svcNamespace, svcName) +} diff --git a/cmd/redis-tools/pkg/commands/helper/acl.go b/cmd/redis-tools/commands/helper/acl.go similarity index 58% rename from cmd/redis-tools/pkg/commands/helper/acl.go rename to cmd/redis-tools/commands/helper/acl.go index a7bc54a..88411ea 100644 --- a/cmd/redis-tools/pkg/commands/helper/acl.go +++ b/cmd/redis-tools/commands/helper/acl.go @@ -1,19 +1,3 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package helper import ( @@ -23,7 +7,8 @@ import ( "strings" "time" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/types/user" + security "github.com/alauda/redis-operator/pkg/security/password" + "github.com/alauda/redis-operator/pkg/types/user" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -36,7 +21,7 @@ func GetUserPassword(ctx context.Context, client *kubernetes.Clientset, namespac if err != nil { return "", err } - users, err := LoadACLUsers(ctx, client, cm) + users, err := LoadACLUsersFromConfigmap(ctx, client, cm) if err != nil { return "", err } @@ -63,7 +48,7 @@ func GenerateACL(ctx context.Context, client *kubernetes.Clientset, namespace, n if err != nil { return nil, err } - users, err := LoadACLUsers(ctx, client, cm) + users, err := LoadACLUsersFromConfigmap(ctx, client, cm) if err != nil { return nil, err } @@ -75,29 +60,30 @@ func GenerateACL(ctx context.Context, client *kubernetes.Clientset, namespace, n return } -// LoadACLUsers load acls from configmap -func LoadACLUsers(ctx context.Context, clientset *kubernetes.Clientset, cm *v1.ConfigMap) ([]*user.User, error) { +// LoadACLUsersFromConfigmap load acls from configmap +func LoadACLUsersFromConfigmap(ctx context.Context, clientset *kubernetes.Clientset, cm *v1.ConfigMap) ([]*user.User, error) { var users []*user.User if cm == nil { return users, nil } for name, userData := range cm.Data { - if name == "" { - name = "default" - } - var u user.User if err := json.Unmarshal([]byte(userData), &u); err != nil { return nil, fmt.Errorf("parse user %s failed, error=%s", name, err) } + u.Name = name if u.Password != nil && u.Password.SecretName != "" { if secret, err := GetSecret(ctx, clientset, cm.Namespace, u.Password.SecretName); err != nil { return nil, err } else { u.Password, _ = user.NewPassword(secret) + if name != "operator" { + if err := security.PasswordValidate(u.Password.String(), 8, 32); err != nil { + continue + } + } } } - u.Name = name if err := u.Validate(); err != nil { return nil, fmt.Errorf(`user "%s" is invalid, %s`, u.Name, err) @@ -150,48 +136,21 @@ func GetConfigmap(ctx context.Context, client *kubernetes.Clientset, namespace, // formatACLSetCommand // // only acl 1 supported -func formatACLSetCommand(user *user.User) (args []string) { +func formatACLSetCommand(user *user.User) []string { // keep in mind that the user.Name is "default" for default user // when update command,password,keypattern, must reset them all - args = []string{"user", user.Name} + var ( + args = []string{"user", user.Name} + ) for _, rule := range user.Rules { - for _, cate := range rule.Categories { - cate = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(cate), "+"), "@") - args = append(args, fmt.Sprintf("+@%s", cate)) - } - for _, cmd := range rule.AllowedCommands { - cmd = strings.TrimPrefix(cmd, "+") - args = append(args, fmt.Sprintf("+%s", cmd)) - } - - isDisableAllCmd := false - for _, cmd := range rule.DisallowedCommands { - cmd = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(cmd), "-"), "@") - if cmd == "nocommands" || cmd == "all" { - isDisableAllCmd = true - } - args = append(args, fmt.Sprintf("-%s", cmd)) - } - if len(rule.Categories) == 0 && len(rule.AllowedCommands) == 0 && !isDisableAllCmd { - args = append(args, "+@all") - } - for _, pattern := range rule.KeyPatterns { - pattern = strings.TrimPrefix(strings.TrimSpace(pattern), "~") - if !strings.HasPrefix(pattern, "%") { - pattern = fmt.Sprintf("~%s", pattern) - } - args = append(args, pattern) - } - - passwd := user.Password.String() - if passwd == "" { - args = append(args, "nopass") - } else { - args = append(args, fmt.Sprintf(">%s", passwd)) - } - - // NOTE: on must after reset - args = append(args, "on") + args = append(args, strings.Fields(rule.Encode())...) } - return + passwd := user.Password.String() + if passwd == "" { + args = append(args, "nopass") + } else { + args = append(args, fmt.Sprintf(">%s", passwd)) + } + // NOTE: on must after reset + return append(args, "on") } diff --git a/cmd/redis-tools/commands/helper/acl_test.go b/cmd/redis-tools/commands/helper/acl_test.go new file mode 100644 index 0000000..c48f09f --- /dev/null +++ b/cmd/redis-tools/commands/helper/acl_test.go @@ -0,0 +1,94 @@ +package helper + +import ( + "reflect" + "testing" + + "github.com/alauda/redis-operator/pkg/types/user" +) + +func Test_formatACLSetCommand(t *testing.T) { + type args struct { + user *user.User + } + tests := []struct { + name string + args args + wantArgs []string + }{ + { + name: "default", + args: args{ + user: &user.User{ + Name: "default", + Role: user.RoleDeveloper, + Rules: []*user.Rule{ + { + DisallowedCommands: []string{"flushall", "flushdb"}, + }, + }, + }, + }, + wantArgs: []string{"user", "default", "-flushall", "-flushdb", "nopass", "on"}, + }, + { + name: "custom1", + args: args{ + user: &user.User{ + Name: "custom1", + Role: user.RoleDeveloper, + Rules: []*user.Rule{ + { + Categories: []string{"read"}, + DisallowedCategories: []string{"all"}, + DisallowedCommands: []string{"keys"}, + KeyPatterns: []string{"*"}, + }, + }, + }, + }, + wantArgs: []string{"user", "custom1", "-@all", "+@read", "-keys", "~*", "nopass", "on"}, + }, + { + name: "custom2", + args: args{ + user: &user.User{ + Name: "custom2", + Role: user.RoleDeveloper, + Rules: []*user.Rule{ + { + AllowedCommands: []string{"cluster"}, + DisallowedCommands: []string{"cluster|setslot", "cluster|nodes"}, + KeyPatterns: []string{"*"}, + }, + }, + }, + }, + wantArgs: []string{"user", "custom2", "+cluster", "-cluster|setslot", "-cluster|nodes", "~*", "nopass", "on"}, + }, + { + name: "custom3", + args: args{ + user: &user.User{ + Name: "custom3", + Role: user.RoleDeveloper, + Rules: []*user.Rule{ + { + DisallowedCommands: []string{"cluster|setslot", "cluster|nodes"}, + KeyPatterns: []string{"*"}, + }, + }, + }, + }, + wantArgs: []string{"user", "custom3", "-cluster|setslot", "-cluster|nodes", "~*", "nopass", "on"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotArgs := formatACLSetCommand(tt.args.user); !reflect.DeepEqual(gotArgs, tt.wantArgs) { + t.Errorf("formatACLSetCommand() = %v, want %v", gotArgs, tt.wantArgs) + } + }) + } +} diff --git a/cmd/redis-tools/pkg/commands/helper/command.go b/cmd/redis-tools/commands/helper/command.go similarity index 55% rename from cmd/redis-tools/pkg/commands/helper/command.go rename to cmd/redis-tools/commands/helper/command.go index 6cf564c..1b96486 100644 --- a/cmd/redis-tools/pkg/commands/helper/command.go +++ b/cmd/redis-tools/commands/helper/command.go @@ -1,19 +1,3 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package helper import ( @@ -21,9 +5,10 @@ import ( "fmt" "os" "strings" + "time" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/kubernetes/client" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/logger" + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/cmd/redis-tools/util" "github.com/urfave/cli/v2" ) @@ -64,9 +49,9 @@ func NewCommand(ctx context.Context) *cli.Command { ctx, cancel := context.WithCancel(ctx) defer cancel() - logger := logger.NewLogger(c) + logger := util.NewLogger(c) - client, err := client.NewClient() + client, err := util.NewClient() if err != nil { logger.Error(err, "create k8s client failed, error=%s", err) return cli.Exit(err, 1) @@ -136,9 +121,9 @@ func NewCommand(ctx context.Context) *cli.Command { ctx, cancel := context.WithCancel(ctx) defer cancel() - logger := logger.NewLogger(c) + logger := util.NewLogger(c) - client, err := client.NewClient() + client, err := util.NewClient() if err != nil { logger.Error(err, "create k8s client failed, error=%s", err) return cli.Exit(err, 1) @@ -152,6 +137,94 @@ func NewCommand(ctx context.Context) *cli.Command { return nil }, }, + { + Name: "healthcheck", + Usage: "Redis health check", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "operator-username", + Usage: "Operator username", + EnvVars: []string{"OPERATOR_USERNAME"}, + }, + &cli.StringFlag{ + Name: "operator-secret-name", + Usage: "Operator user password secret name", + EnvVars: []string{"OPERATOR_SECRET_NAME"}, + }, + &cli.BoolFlag{ + Name: "tls", + Usage: "Enable tls", + EnvVars: []string{"TLS_ENABLED"}, + }, + &cli.StringFlag{ + Name: "tls-key-file", + Usage: "Name of the client key file (including full path)", + EnvVars: []string{"TLS_CLIENT_KEY_FILE"}, + Value: "/tls/tls.key", + }, + &cli.StringFlag{ + Name: "tls-cert-file", + Usage: "Name of the client certificate file (including full path)", + EnvVars: []string{"TLS_CLIENT_CERT_FILE"}, + Value: "/tls/tls.crt", + }, + &cli.StringFlag{ + Name: "tls-ca-file", + Usage: "Name of the ca file (including full path)", + EnvVars: []string{"TLS_CA_CERT_FILE"}, + Value: "/tls/ca.crt", + }, + &cli.StringFlag{ + Name: "addr", + Usage: "Redis instance service address", + Value: "local.inject:6379", + }, + &cli.IntFlag{ + Name: "timeout", + Aliases: []string{"t"}, + Usage: "Timeout time of ping", + Value: 3, + }, + }, + Subcommands: []*cli.Command{ + { + Name: "ping", + Usage: "ping redis instance to check if it is alive", + Action: func(c *cli.Context) error { + var ( + serviceAddr = c.String("addr") + timeout = c.Int64("timeout") + ) + logger := util.NewLogger(c).WithName("liveness") + + if timeout <= 0 { + timeout = 5 + } + if serviceAddr == "" { + serviceAddr = "local.inject:6379" + } + ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) + defer cancel() + + info, err := commands.LoadAuthInfo(c, ctx) + if err != nil { + logger.Error(err, "load auth info failed") + return cli.Exit(err, 1) + } + + if err := Ping(ctx, serviceAddr, *info); err != nil { + if strings.HasPrefix(err.Error(), "LOADING") || + strings.HasPrefix(err.Error(), "BUSY") { + return nil + } + logger.Error(err, "ping failed") + return cli.Exit(err, 1) + } + return nil + }, + }, + }, + }, }, } } diff --git a/cmd/redis-tools/commands/helper/healthcheck.go b/cmd/redis-tools/commands/helper/healthcheck.go new file mode 100644 index 0000000..78285fe --- /dev/null +++ b/cmd/redis-tools/commands/helper/healthcheck.go @@ -0,0 +1,18 @@ +package helper + +import ( + "context" + + "github.com/alauda/redis-operator/pkg/redis" +) + +// Ping +func Ping(ctx context.Context, addr string, authInfo redis.AuthInfo) error { + client := redis.NewRedisClient(addr, authInfo) + defer client.Close() + + if _, err := client.Do(ctx, "PING"); err != nil { + return err + } + return nil +} diff --git a/cmd/redis-tools/pkg/commands/runner/command.go b/cmd/redis-tools/commands/runner/command.go similarity index 77% rename from cmd/redis-tools/pkg/commands/runner/command.go rename to cmd/redis-tools/commands/runner/command.go index 536016a..16b1728 100644 --- a/cmd/redis-tools/pkg/commands/runner/command.go +++ b/cmd/redis-tools/commands/runner/command.go @@ -1,26 +1,11 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package runner import ( "context" + "time" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/logger" + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/cmd/redis-tools/util" "github.com/urfave/cli/v2" ) @@ -99,7 +84,7 @@ func NewCommand(ctx context.Context) *cli.Command { Value: "/data", }, &cli.StringFlag{ - Name: "node-config-name", + Name: "config-name", Usage: "Node config file name", Value: "nodes.conf", }, @@ -119,11 +104,12 @@ func NewCommand(ctx context.Context) *cli.Command { ctx, cancel := context.WithCancel(ctx) defer cancel() - logger := logger.NewLogger(c) + logger := util.NewLogger(c) if c.Bool("sync-l2c") { go func() { - _ = SyncFromLocalToConfigMap(c, ctx, logger) + time.Sleep(time.Second * 60) + _ = SyncFromLocalToEtcd(c, ctx, "", true, logger) }() } diff --git a/cmd/redis-tools/pkg/commands/runner/rebalance.go b/cmd/redis-tools/commands/runner/rebalance.go similarity index 84% rename from cmd/redis-tools/pkg/commands/runner/rebalance.go rename to cmd/redis-tools/commands/runner/rebalance.go index a4989ef..fcc3dbd 100644 --- a/cmd/redis-tools/pkg/commands/runner/rebalance.go +++ b/cmd/redis-tools/commands/runner/rebalance.go @@ -1,19 +1,3 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package runner import ( @@ -26,8 +10,8 @@ import ( "strings" "time" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/types/slot" + "github.com/alauda/redis-operator/pkg/redis" + "github.com/alauda/redis-operator/pkg/slot" "github.com/go-logr/logr" v1 "k8s.io/api/core/v1" ) @@ -42,7 +26,7 @@ const ( // RebalanceSlots func RebalanceSlots(ctx context.Context, authInfo redis.AuthInfo, address string, logger logr.Logger) error { // check current nodes info - client := redis.NewClient(address, authInfo) + client := redis.NewRedisClient(address, authInfo) defer client.Close() // user timer todo wait @@ -111,8 +95,8 @@ func RebalanceSlots(ctx context.Context, authInfo redis.AuthInfo, address string var ( slotIds = slots.Slots(slot.SlotImporting) lastNodeId string - sourceClient redis.Client - sourceNode *redis.Node + sourceClient redis.RedisClient + sourceNode *redis.ClusterNode ) for i, slotId := range slotIds { _, sourceNodeId := slots.MoveingStatus(slotId) @@ -136,7 +120,7 @@ func RebalanceSlots(ctx context.Context, authInfo redis.AuthInfo, address string // TRICK: local connect to nodeport not work if ret, err := redis.Strings(client.Do(ctx, "CONFIG", "GET", "bind")); err != nil { - logger.Error(err, "get node bind address failed", "id", sourceNode.ID) + logger.Error(err, "get node bind address failed", "id", sourceNode.Id) return "", err } else if len(ret) == 2 { bindIPs := strings.Fields(ret[1]) @@ -200,12 +184,12 @@ func getLocalAddressByIPFamily(family string, ips []string, def string) string { return def } -func importSlots(ctx context.Context, nodes redis.Nodes, currentNode, sourceNode *redis.Node, client, sourceClient redis.Client, slotId int, authInfo redis.AuthInfo, logger logr.Logger) error { +func importSlots(ctx context.Context, nodes redis.ClusterNodes, currentNode, sourceNode *redis.ClusterNode, client, sourceClient redis.RedisClient, slotId int, authInfo redis.AuthInfo, logger logr.Logger) error { broadcastSlotStatus := func() { for _, node := range nodes { if node.Role == redis.SlaveRole || - node.ID == sourceNode.ID || - node.ID == currentNode.ID || + node.Id == sourceNode.Id || + node.Id == currentNode.Id || node.IsFailed() || !node.IsConnected() { continue } @@ -215,7 +199,7 @@ func importSlots(ctx context.Context, nodes redis.Nodes, currentNode, sourceNode defer tmpClient.Close() // ignore errors - if err := RetrySetSlotStatus(ctx, tmpClient, slotId, currentNode.ID, 2); err != nil { + if err := RetrySetSlotStatus(ctx, tmpClient, slotId, currentNode.Id, 2); err != nil { logger.Error(err, "broadcast slot status failed", "slot", slotId, "node", node.Addr) } }() @@ -242,14 +226,14 @@ func importSlots(ctx context.Context, nodes redis.Nodes, currentNode, sourceNode masterNodes := sourceNodesInfo.Masters() for _, node := range masterNodes { if node.Slots().Status(slotId) == slot.SlotAssigned { - return node.ID + return node.Id } } return "" }() - if currentSlotNodeId == currentNode.ID { + if currentSlotNodeId == currentNode.Id { // update slot info - if err := RetrySetSlotStatus(ctx, client, slotId, currentNode.ID, 5); err != nil { + if err := RetrySetSlotStatus(ctx, client, slotId, currentNode.Id, 5); err != nil { logger.Error(err, "clean slot importing flags failed", "slot", slotId) return err } @@ -259,7 +243,7 @@ func importSlots(ctx context.Context, nodes redis.Nodes, currentNode, sourceNode } err := fmt.Errorf("slot not owned by node") - logger.Error(err, "check slot status failed", "slot", slotId, "node", sourceNode.ID, "status", status) + logger.Error(err, "check slot status failed", "slot", slotId, "node", sourceNode.Id, "status", status) return err } } @@ -267,7 +251,7 @@ func importSlots(ctx context.Context, nodes redis.Nodes, currentNode, sourceNode localAddr := getLocalAddressByIPFamily(os.Getenv("IP_FAMILY_PREFER"), strings.Split(os.Getenv("POD_IPS"), ","), os.Getenv("POD_IP")) - logger.Info("import slot", "slot", slotId, "source", sourceNode.ID, "desc", currentNode.ID) + logger.Info("import slot", "slot", slotId, "source", sourceNode.Id, "desc", currentNode.Id) for { // get keys of this slot keys, err := redis.Values(sourceClient.Do(ctx, "cluster", "getkeysinslot", slotId, 3)) @@ -298,22 +282,24 @@ func importSlots(ctx context.Context, nodes redis.Nodes, currentNode, sourceNode time.Sleep(time.Millisecond * 50) } - // update slot info - if err := RetrySetSlotStatus(ctx, sourceClient, slotId, currentNode.ID, 5); err != nil { - logger.Error(err, "clean slot migrating flags failed", "slot", slotId) + if err := RetrySetSlotStatus(ctx, client, slotId, currentNode.Id, 5); err != nil { + logger.Error(err, "clean slot importing flags failed", "slot", slotId) return err } - if err := RetrySetSlotStatus(ctx, client, slotId, currentNode.ID, 5); err != nil { - logger.Error(err, "clean slot importing flags failed", "slot", slotId) + // update slot info + if err := RetrySetSlotStatus(ctx, sourceClient, slotId, currentNode.Id, 5); err != nil { + logger.Error(err, "clean slot migrating flags failed", "slot", slotId) return err } broadcastSlotStatus() + time.Sleep(time.Millisecond * 100) + return nil } -func RetrySetSlotStatus(ctx context.Context, client redis.Client, slotId int, nodeId string, retryCount int) (err error) { +func RetrySetSlotStatus(ctx context.Context, client redis.RedisClient, slotId int, nodeId string, retryCount int) (err error) { for i := 0; i < retryCount; i++ { if _, err = client.Do(ctx, "CLUSTER", "SETSLOT", slotId, "NODE", nodeId); err == nil { return nil @@ -325,7 +311,7 @@ func RetrySetSlotStatus(ctx context.Context, client redis.Client, slotId int, no return } -func isClusterStateOk(ctx context.Context, cli redis.Client) (bool, error) { +func isClusterStateOk(ctx context.Context, cli redis.RedisClient) (bool, error) { nctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() diff --git a/cmd/redis-tools/commands/runner/sync.go b/cmd/redis-tools/commands/runner/sync.go new file mode 100644 index 0000000..9d3d061 --- /dev/null +++ b/cmd/redis-tools/commands/runner/sync.go @@ -0,0 +1,84 @@ +package runner + +import ( + "context" + "os" + "path" + "strings" + "time" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/cmd/redis-tools/sync" + "github.com/alauda/redis-operator/cmd/redis-tools/util" + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func SyncFromLocalToEtcd(c *cli.Context, ctx context.Context, resourceKind string, watch bool, logger logr.Logger) error { + var ( + namespace = c.String("namespace") + podName = c.String("pod-name") + workspace = c.String("workspace") + filename = c.String("config-name") + resourcePrefix = c.String("prefix") + syncInterval = c.Int64("interval") + ) + + client, err := util.NewClient() + if err != nil { + logger.Error(err, "create k8s client failed, error=%s", err) + return cli.Exit(err, 1) + } + + // sync to local + name := strings.Join([]string{strings.TrimSuffix(resourcePrefix, "-"), podName}, "-") + ownRefs, err := commands.NewOwnerReference(ctx, client, namespace, podName) + if err != nil { + return cli.Exit(err, 1) + } + if watch { + // start sync process + return WatchAndSync(ctx, client, resourceKind, namespace, name, workspace, filename, syncInterval, ownRefs, logger) + } + + // write once + filePath := path.Join(workspace, filename) + data, err := os.ReadFile(filePath) + if err != nil { + logger.Error(err, "read file failed", "file", filePath) + return err + } + obj := sync.PersistentObject{} + obj.Set(filename, data) + return obj.Save(ctx, client, resourceKind, namespace, name, ownRefs, logger) +} + +func WatchAndSync(ctx context.Context, client *kubernetes.Clientset, resourceKind, namespace, name, workspace, target string, + syncInterval int64, ownerRefs []metav1.OwnerReference, logger logr.Logger) error { + + ctrl, err := sync.NewController(client, sync.ControllerOptions{ + ResourceKind: resourceKind, + Namespace: namespace, + Name: name, + OwnerReferences: ownerRefs, + SyncInterval: time.Duration(syncInterval) * time.Second, + Filters: []sync.Filter{&sync.RedisClusterFilter{}}, + }, logger) + if err != nil { + return err + } + fileWathcer, _ := sync.NewFileWatcher(ctrl.Handler, logger) + + logger.Info("watch file", "file", path.Join(workspace, target)) + if err := fileWathcer.Add(path.Join(workspace, target)); err != nil { + logger.Error(err, "watch file failed, error=%s") + return cli.Exit(err, 1) + } + + go func() { + _ = fileWathcer.Run(ctx) + }() + return ctrl.Run(ctx) +} diff --git a/cmd/redis-tools/commands/sentinel/access.go b/cmd/redis-tools/commands/sentinel/access.go new file mode 100644 index 0000000..4fcef31 --- /dev/null +++ b/cmd/redis-tools/commands/sentinel/access.go @@ -0,0 +1,164 @@ +package sentinel + +import ( + "context" + "fmt" + "net/netip" + "os" + "strconv" + "strings" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" +) + +func Access(ctx context.Context, client *kubernetes.Clientset, namespace, podName, ipfamily string, serviceType corev1.ServiceType, logger logr.Logger) error { + logger.Info("service access", "serviceType", serviceType, "ipfamily", ipfamily, "podName", podName) + pod, err := commands.GetPod(ctx, client, namespace, podName, logger) + if err != nil { + logger.Error(err, "get pods failed", "namespace", namespace, "name", podName) + return err + } + + if pod.Status.HostIP == "" { + return fmt.Errorf("pod not found or pod with invalid hostIP") + } + + var ( + announceIp = pod.Status.PodIP + announcePort int32 = 26379 + ) + if serviceType == corev1.ServiceTypeNodePort { + podSvc, err := commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeNodePort, 20, logger) + if errors.IsNotFound(err) { + if podSvc, err = commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeNodePort, 20, logger); err != nil { + logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + } else if err != nil { + logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + for _, v := range podSvc.Spec.Ports { + if v.Name == "sentinel" { + announcePort = v.NodePort + } + } + + node, err := client.CoreV1().Nodes().Get(ctx, pod.Spec.NodeName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "get nodes err", "node", node.Name) + return err + } + logger.Info("get nodes success", "Name", node.Name) + + var addresses []string + for _, addr := range node.Status.Addresses { + if addr.Address == "" { + continue + } + + switch addr.Type { + case corev1.NodeExternalIP: + ip, err := netip.ParseAddr(addr.Address) + if err != nil { + logger.Error(err, "parse address err", "address", addr.Address) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + addresses = append(addresses, addr.Address) + } else if ipfamily != "IPv6" && ip.Is4() { + addresses = append(addresses, addr.Address) + } + case corev1.NodeInternalIP: + // internal ip first + ip, err := netip.ParseAddr(addr.Address) + if err != nil { + logger.Error(err, "parse address err", "address", addr.Address) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + addresses = append([]string{addr.Address}, addresses...) + addresses = append(addresses, addr.Address) + } else if ipfamily != "IPv6" && ip.Is4() { + addresses = append([]string{addr.Address}, addresses...) + } + } + } + if len(addresses) > 0 { + announceIp = addresses[0] + } else { + err := fmt.Errorf("no available address") + logger.Error(err, "get usable address failed") + return err + } + } else if serviceType == corev1.ServiceTypeLoadBalancer { + podSvc, err := commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeLoadBalancer, 20, logger) + if errors.IsNotFound(err) { + if podSvc, err = commands.RetryGetService(ctx, client, namespace, podName, corev1.ServiceTypeLoadBalancer, 20, logger); err != nil { + logger.Error(err, "retry get lb service failed") + return err + } + } else if err != nil { + logger.Error(err, "get lb service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) + return err + } + + for _, v := range podSvc.Status.LoadBalancer.Ingress { + if v.IP == "" { + continue + } + + ip, err := netip.ParseAddr(v.IP) + if err != nil { + logger.Error(err, "parse address err", "address", v.IP) + return err + } + if ipfamily == "IPv6" && ip.Is6() { + announceIp = v.IP + break + } + if ipfamily != "IPv6" && ip.Is4() { + announceIp = v.IP + break + } + } + } else { + for _, addr := range pod.Status.PodIPs { + ip, err := netip.ParseAddr(addr.IP) + if err != nil { + return err + } + if ipfamily == "IPv6" && ip.Is6() { + announceIp = addr.IP + break + } else if ipfamily != "IPv6" && ip.Is4() { + announceIp = addr.IP + break + } + } + } + + format_announceIp := strings.Replace(announceIp, ":", "-", -1) + labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s"},{"op":"add","path":"/metadata/labels/%s","value":"%s"}]`, + strings.Replace("middleware.alauda.io/announce_ip", "/", "~1", -1), format_announceIp, + strings.Replace("middleware.alauda.io/announce_port", "/", "~1", -1), strconv.Itoa(int(announcePort))) + + logger.Info(labelPatch) + _, err = client.CoreV1().Pods(pod.Namespace).Patch(ctx, podName, types.JSONPatchType, []byte(labelPatch), metav1.PatchOptions{}) + if err != nil { + logger.Error(err, "patch pod label failed") + return err + } + sentinelConfigContent := fmt.Sprintf(` +announce-ip %s +announce-port %d +`, announceIp, announcePort) + + return os.WriteFile("/data/announce.conf", []byte(sentinelConfigContent), 0644) // #nosec G306 +} diff --git a/cmd/redis-tools/commands/sentinel/command.go b/cmd/redis-tools/commands/sentinel/command.go new file mode 100644 index 0000000..4605e4b --- /dev/null +++ b/cmd/redis-tools/commands/sentinel/command.go @@ -0,0 +1,400 @@ +package sentinel + +import ( + "context" + "fmt" + "net" + "net/url" + "strings" + "time" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/cmd/redis-tools/commands/runner" + "github.com/alauda/redis-operator/cmd/redis-tools/util" + "github.com/alauda/redis-operator/pkg/redis" + "github.com/urfave/cli/v2" + corev1 "k8s.io/api/core/v1" +) + +func NewCommand(ctx context.Context) *cli.Command { + return &cli.Command{ + Name: "sentinel", + Usage: "Sentinel set commands", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Usage: "Namespace of current pod", + EnvVars: []string{"NAMESPACE"}, + }, + &cli.StringFlag{ + Name: "pod-name", + Usage: "The name of current pod", + EnvVars: []string{"POD_NAME"}, + }, + &cli.StringFlag{ + Name: "pod-uid", + Usage: "The id of current pod", + EnvVars: []string{"POD_UID"}, + }, + &cli.StringFlag{ + Name: "service-name", + Usage: "Service name of the statefulset", + EnvVars: []string{"SERVICE_NAME"}, + }, + &cli.StringFlag{ + Name: "operator-username", + Usage: "Operator username", + EnvVars: []string{"OPERATOR_USERNAME"}, + }, + &cli.StringFlag{ + Name: "operator-secret-name", + Usage: "Operator user password secret name", + EnvVars: []string{"OPERATOR_SECRET_NAME"}, + }, + &cli.BoolFlag{ + Name: "tls", + Usage: "Enable tls", + EnvVars: []string{"TLS_ENABLED"}, + }, + &cli.StringFlag{ + Name: "tls-key-file", + Usage: "Name of the client key file (including full path)", + EnvVars: []string{"TLS_CLIENT_KEY_FILE"}, + Value: "/tls/tls.key", + }, + &cli.StringFlag{ + Name: "tls-cert-file", + Usage: "Name of the client certificate file (including full path)", + EnvVars: []string{"TLS_CLIENT_CERT_FILE"}, + Value: "/tls/tls.crt", + }, + &cli.StringFlag{ + Name: "tls-ca-file", + Usage: "Name of the ca file (including full path)", + EnvVars: []string{"TLS_CA_CERT_FILE"}, + Value: "/tls/ca.crt", + }, + &cli.StringFlag{ + Name: "ip-family", + Usage: "IP_FAMILY for servie access", + EnvVars: []string{"IP_FAMILY_PREFER"}, + }, + &cli.StringFlag{ + Name: "service-type", + Usage: "Service type for sentinel service", + EnvVars: []string{"SERVICE_TYPE"}, + Value: "ClusterIP", + }, + }, + Subcommands: []*cli.Command{ + { + Name: "expose", + Usage: "Create nodeport service for current pod to announce", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) error { + var ( + namespace = c.String("namespace") + podName = c.String("pod-name") + ipFamily = c.String("ip-family") + serviceType = corev1.ServiceType(c.String("service-type")) + ) + if namespace == "" { + return cli.Exit("require namespace", 1) + } + if podName == "" { + return cli.Exit("require podname", 1) + } + + logger := util.NewLogger(c).WithName("Access") + + client, err := util.NewClient() + if err != nil { + logger.Error(err, "create k8s client failed, error=%s", err) + return cli.Exit(err, 1) + } + + if err := Access(ctx, client, namespace, podName, ipFamily, serviceType, logger); err != nil { + logger.Error(err, "enable nodeport service access failed") + return cli.Exit(err, 1) + } + return nil + }, + }, + { + Name: "get-master-addr", + Usage: "Get current master address", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "monitor-uri", + Usage: "Monitor URI", + EnvVars: []string{"MONITOR_URI"}, + }, + &cli.StringFlag{ + Name: "name", + Usage: "Sentinel master name", + Value: "mymaster", + }, + &cli.StringFlag{ + Name: "monitor-operator-secret-name", + Usage: "Monitor operator secret name", + EnvVars: []string{"MONITOR_OPERATOR_SECRET_NAME"}, + }, + &cli.BoolFlag{ + Name: "healthy", + Usage: "Require the master is healthy", + }, + &cli.IntFlag{ + Name: "timeout", + Usage: "Timeout for get master address", + Value: 10, + }, + }, + Action: func(c *cli.Context) error { + var ( + sentinelUri = c.String("monitor-uri") + clusterName = c.String("name") + timeout = c.Int("timeout") + requireHealthy = c.Bool("healthy") + ) + + client, err := util.NewClient() + if err != nil { + return cli.Exit(err, 1) + } + + authInfo, err := commands.LoadAuthInfo(c, ctx) + if err != nil { + return cli.Exit(err, 1) + } + + senAuthInfo, err := commands.LoadMonitorAuthInfo(c, ctx, client) + if err != nil { + return cli.Exit(err, 1) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + var sentinelNodes []string + if val, err := url.Parse(sentinelUri); err != nil { + return cli.Exit(fmt.Sprintf("parse sentinel uri failed, error=%s", err), 1) + } else { + sentinelNodes = strings.Split(val.Host, ",") + } + + var redisCli redis.RedisClient + for _, node := range sentinelNodes { + redisCli = redis.NewRedisClient(node, *senAuthInfo) + if _, err = redisCli.Do(ctx, "PING"); err != nil { + redisCli.Close() + redisCli = nil + continue + } + break + } + if redisCli == nil { + return cli.Exit("no sentinel node available", 1) + } + defer redisCli.Close() + + if vals, err := redis.StringMap(redisCli.Do(ctx, "SENTINEL", "master", clusterName)); err != nil { + return cli.Exit(fmt.Sprintf("connect to sentinel failed, error=%s", err), 1) + } else { + addr := net.JoinHostPort(vals["ip"], vals["port"]) + if !requireHealthy { + fmt.Println(addr) + } else if vals["flags"] == "master" { + redisCli = redis.NewRedisClient(addr, *authInfo) + defer redisCli.Close() + + if err := func() error { + var ( + err error + info *redis.RedisInfo + ) + for i := 0; i < 3; i++ { + if info, err = redisCli.Info(ctx); err != nil { + time.Sleep(time.Second) + continue + } else if info.Role != "master" { + return fmt.Errorf("role of node %s is %s", addr, info.Role) + } + return nil + } + return err + }(); err != nil { + return cli.Exit(err, 1) + } else { + fmt.Println(addr) + } + } + } + return nil + }, + }, + { + Name: "failover", + Usage: "Do sentinel failover", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "monitor-uri", + Usage: "Monitor URI", + EnvVars: []string{"MONITOR_URI"}, + }, + &cli.StringFlag{ + Name: "monitor-operator-secret-name", + Usage: "Monitor operator secret name", + EnvVars: []string{"MONITOR_OPERATOR_SECRET_NAME"}, + }, + &cli.StringSliceFlag{ + Name: "escape", + Usage: "Addresses need to be escape", + }, + &cli.StringFlag{ + Name: "name", + Usage: "Sentinel monitor name", + Value: "mymaster", + }, + &cli.IntFlag{ + Name: "timeout", + Usage: "Timeout for get master address", + Value: 120, + }, + }, + Action: func(c *cli.Context) error { + logger := util.NewLogger(c).WithName("Failover") + + client, err := util.NewClient() + if err != nil { + logger.Error(err, "create k8s client failed, error=%s", err) + return cli.Exit(err, 1) + } + + var timeout = c.Int("timeout") + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + return Failover(ctx, c, client, logger) + }, + }, + { + Name: "shutdown", + Usage: "Shutdown sentinel node", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "workspace", + Usage: "Workspace of this container", + Value: "/data", + }, + &cli.StringFlag{ + Name: "config-name", + Usage: "sentinel config file", + Value: "sentinel.conf", + }, + &cli.StringFlag{ + Name: "prefix", + Usage: "Etcd sync resource name prefix", + Value: "sync-", + }, + &cli.IntFlag{ + Name: "timeout", + Aliases: []string{"t"}, + Usage: "Timeout time of shutdown", + Value: 30, + }, + }, + Action: func(c *cli.Context) error { + logger := util.NewLogger(c).WithName("Shutdown") + + client, err := util.NewClient() + if err != nil { + logger.Error(err, "create k8s client failed, error=%s", err) + return cli.Exit(err, 1) + } + if err := Shutdown(ctx, c, client, logger); err != nil { + return cli.Exit(err, 1) + } + return nil + }, + }, + { + Name: "merge-config", + Usage: "merge config from local and cached", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "workspace", + Usage: "Redis server data workdir", + Value: "/data", + }, + &cli.StringFlag{ + Name: "local-conf-file", + Usage: "Local sentinel config file", + Value: "/conf/sentinel.conf", + }, + &cli.StringFlag{ + Name: "config-name", + Usage: "Sentinel config file name", + Value: "sentinel.conf", + }, + &cli.StringFlag{ + Name: "prefix", + Usage: "Resource name prefix", + Value: "sync-", + }, + }, + Action: func(c *cli.Context) error { + + logger := util.NewLogger(c).WithName("Access") + + client, err := util.NewClient() + if err != nil { + logger.Error(err, "create k8s client failed, error=%s", err) + return cli.Exit(err, 1) + } + if err := MergeConfig(ctx, c, client, logger); err != nil { + logger.Error(err, "enable nodeport service access failed") + return cli.Exit(err, 1) + } + return nil + }, + }, + { + Name: "agent", + Usage: "Sentinel agent", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "workspace", + Usage: "Redis server data workdir", + Value: "/data", + }, + &cli.StringFlag{ + Name: "config-name", + Usage: "Sentinel config file name", + Value: "sentinel.conf", + }, + &cli.StringFlag{ + Name: "prefix", + Usage: "Resource name prefix", + Value: "sync-", + }, + &cli.Int64Flag{ + Name: "interval", + Usage: "Sync interval", + Value: 5, + DefaultText: "5s", + }, + }, + Action: func(c *cli.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + logger := util.NewLogger(c) + if err := runner.SyncFromLocalToEtcd(c, ctx, "secret", true, logger); err != nil { + return cli.Exit(err, 1) + } + return nil + }, + }, + }, + } +} diff --git a/cmd/redis-tools/commands/sentinel/config.go b/cmd/redis-tools/commands/sentinel/config.go new file mode 100644 index 0000000..22f9929 --- /dev/null +++ b/cmd/redis-tools/commands/sentinel/config.go @@ -0,0 +1,110 @@ +package sentinel + +import ( + "bytes" + "context" + "fmt" + "os" + "path" + + "github.com/alauda/redis-operator/cmd/redis-tools/sync" + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + "k8s.io/client-go/kubernetes" +) + +const ( + REDIS_CONFIG_REWRITE_SIGNATURE = "# Generated by CONFIG REWRITE" +) + +type HealOptions struct { + Namespace string + PodName string + Workspace string + TargetName string + Prefix string +} + +// MergeConfig merge sentinel local and cached config +func MergeConfig(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { + localConf := c.String("local-conf-file") + + opts := &HealOptions{ + Namespace: c.String("namespace"), + PodName: c.String("pod-name"), + Workspace: c.String("workspace"), + TargetName: c.String("config-name"), + Prefix: c.String("prefix"), + } + + var ( + err error + localFileData []byte + cachedData []byte + ) + if localFileData, err = os.ReadFile(localConf); err != nil && !os.IsNotExist(err) { + logger.Error(err, "read local config failed") + return err + } + localFileData = append(localFileData, '\n') + + if cachedData, err = loadCachedData(ctx, client, opts); err != nil { + logger.Error(err, "load cached config failed") + return err + } + + parts := bytes.SplitN(cachedData, []byte(REDIS_CONFIG_REWRITE_SIGNATURE), 2) + if len(parts) == 2 { + for _, line := range bytes.Split(parts[0], []byte{'\n'}) { + if len(line) == 0 || line[0] == '#' { + continue + } + if bytes.HasPrefix(line, []byte("sentinel ")) { + localFileData = append(localFileData, line...) + localFileData = append(localFileData, '\n') + } + } + + localFileData = append(localFileData, REDIS_CONFIG_REWRITE_SIGNATURE...) + localFileData = append(localFileData, '\n') + for _, line := range bytes.Split(parts[1], []byte{'\n'}) { + if len(line) == 0 || line[0] == '#' || + bytes.Equal(line, []byte("sentinel ")) || bytes.Equal(line, []byte("sentinel")) { + continue + } + if bytes.HasPrefix(line, []byte("port ")) || + bytes.HasPrefix(line, []byte("bind ")) || + bytes.HasPrefix(line, []byte("dir ")) || + bytes.HasPrefix(line, []byte("user")) || + bytes.HasPrefix(line, []byte("requirepass")) || + bytes.HasPrefix(line, []byte("sentinel announce-ip")) || + bytes.HasPrefix(line, []byte("sentinel announce-port")) || + bytes.HasPrefix(line, []byte("sentinel sentinel-user")) || + bytes.HasPrefix(line, []byte("sentinel sentinel-pass")) { + continue + } + localFileData = append(localFileData, line...) + localFileData = append(localFileData, '\n') + } + } + + tmpFile := path.Join(opts.Workspace, "tmp-"+opts.TargetName) + targetFile := path.Join(opts.Workspace, opts.TargetName) + if err := os.WriteFile(tmpFile, []byte(localFileData), 0600); err != nil { + logger.Error(err, "update sentinel.conf failed") + return err + } else if err := os.Rename(tmpFile, targetFile); err != nil { + logger.Error(err, "rename tmp-sentinel.conf to sentinel.conf failed") + return err + } + return nil +} + +func loadCachedData(ctx context.Context, client *kubernetes.Clientset, opts *HealOptions) ([]byte, error) { + name := fmt.Sprintf("%s%s", opts.Prefix, opts.PodName) + obj, err := sync.LoadPersistentObject(ctx, client, "secret", opts.Namespace, name) + if err != nil { + return nil, err + } + return obj.Get(opts.TargetName), nil +} diff --git a/cmd/redis-tools/commands/sentinel/failover.go b/cmd/redis-tools/commands/sentinel/failover.go new file mode 100644 index 0000000..0c08f20 --- /dev/null +++ b/cmd/redis-tools/commands/sentinel/failover.go @@ -0,0 +1,141 @@ +package sentinel + +import ( + "context" + "fmt" + "net/url" + "slices" + "strings" + "time" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands" + "github.com/alauda/redis-operator/pkg/redis" + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + "k8s.io/client-go/kubernetes" +) + +func Failover(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { + var ( + sentinelUri = c.String("monitor-uri") + name = c.String("name") + failoverAddresses = c.StringSlice("escape") + ) + if sentinelUri == "" { + return nil + } + + var sentinelNodes []string + if val, err := url.Parse(sentinelUri); err != nil { + return cli.Exit(fmt.Sprintf("parse sentinel uri failed, error=%s", err), 1) + } else { + sentinelNodes = strings.Split(val.Host, ",") + } + + senAuthInfo, err := commands.LoadMonitorAuthInfo(c, ctx, client) + if err != nil { + logger.Error(err, "load sentinel auth info failed") + return cli.Exit(fmt.Sprintf("load sentinel auth info failed, error=%s", err), 1) + } + + var redisCli redis.RedisClient + for _, node := range sentinelNodes { + redisCli = redis.NewRedisClient(node, *senAuthInfo) + if _, err = redisCli.Do(ctx, "PING"); err != nil { + logger.Error(err, "ping sentinel node failed", "node", node) + redisCli.Close() + redisCli = nil + continue + } + break + } + if redisCli == nil { + return cli.Exit("no sentinel node available", 1) + } + defer redisCli.Close() + + if info, err := redisCli.Info(ctx); err != nil { + logger.Error(err, "get sentinel info failed") + return cli.Exit("get sentinel info failed", 1) + } else { + currentMaster := info.SentinelMaster0.Address.String() + logger.Info("check current master", "addr", currentMaster) + if currentMaster != "" { + if info.SentinelMaster0.Status == "ok" { + if !slices.Contains(failoverAddresses, currentMaster) { + logger.Info("current master is ok, no need to failover") + return nil + } + } else { + logger.Info("current master is not healthy, try failover", "addr", currentMaster) + failoverAddresses = append(failoverAddresses, currentMaster) + } + } else { + logger.Info("master not registered", "name", name) + return nil + } + } + + needFailover := func() (bool, error) { + logger.Info("assess if failover is required") + info, err := redisCli.Info(ctx) + if err != nil { + logger.Error(err, "get sentinel info failed") + return false, err + } + logger.Info("current master", "master", info.SentinelMaster0) + if info.SentinelMaster0.Name != name { + logger.Info("master not registered yet, abort failover") + return false, nil + } + masterAddr := info.SentinelMaster0.Address.String() + if slices.Contains(failoverAddresses, masterAddr) || info.SentinelMaster0.Status != "ok" { + // check slaves + if info.SentinelMaster0.Replicas == 0 { + logger.Info("no suitable replica to promote, abort failover") + return false, nil + } + // TODO: check slaves status + return true, nil + } + return false, nil + } + + // get current master + failoverSucceed := false +__FAILOVER_END__: + for i := 0; i < 6; i++ { + if nf, err := needFailover(); err != nil { + logger.Error(err, "check failover failed, retry later") + time.Sleep(time.Second * 5) + continue + } else if nf { + if _, err := redisCli.Do(ctx, "SENTINEL", "FAILOVER", name); err != nil { + logger.Error(err, "do sentinel failover failed, retry later") + time.Sleep(time.Second * 5) + continue + } + for j := 0; j < 6; j++ { + if nf, err := needFailover(); err != nil { + time.Sleep(time.Second * 5) + continue + } else if nf { + time.Sleep(time.Second * 5) + continue + } else { + failoverSucceed = true + break __FAILOVER_END__ + } + } + } else { + failoverSucceed = true + break + } + } + + if !failoverSucceed { + logger.Info("failover failed, start redis node directly") + return cli.Exit("failover failed", 1) + } + return nil +} diff --git a/cmd/redis-tools/commands/sentinel/shutdown.go b/cmd/redis-tools/commands/sentinel/shutdown.go new file mode 100644 index 0000000..b904b75 --- /dev/null +++ b/cmd/redis-tools/commands/sentinel/shutdown.go @@ -0,0 +1,28 @@ +package sentinel + +import ( + "context" + "time" + + "github.com/alauda/redis-operator/cmd/redis-tools/commands/runner" + "github.com/go-logr/logr" + "github.com/urfave/cli/v2" + "k8s.io/client-go/kubernetes" +) + +// Shutdown +func Shutdown(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { + timeout := time.Duration(c.Int("timeout")) * time.Second + if timeout == 0 { + timeout = time.Second * 30 + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + // sync current nodes.conf to configmap + logger.Info("persistent sentinel.conf to secret") + if err := runner.SyncFromLocalToEtcd(c, ctx, "secret", false, logger); err != nil { + logger.Error(err, "persistent sentinel.conf to configmap failed") + } + return nil +} diff --git a/cmd/redis-tools/main.go b/cmd/redis-tools/main.go index 48f1271..903eb66 100644 --- a/cmd/redis-tools/main.go +++ b/cmd/redis-tools/main.go @@ -1,19 +1,3 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package main import ( @@ -24,11 +8,11 @@ import ( "path/filepath" "syscall" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands/backup" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands/cluster" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands/helper" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands/runner" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands/sync" + "github.com/alauda/redis-operator/cmd/redis-tools/commands/cluster" + "github.com/alauda/redis-operator/cmd/redis-tools/commands/failover" + "github.com/alauda/redis-operator/cmd/redis-tools/commands/helper" + "github.com/alauda/redis-operator/cmd/redis-tools/commands/runner" + "github.com/alauda/redis-operator/cmd/redis-tools/commands/sentinel" "github.com/urfave/cli/v2" ) @@ -76,10 +60,10 @@ func main() { app := NewApp( ctx, cluster.NewCommand(ctx), + sentinel.NewCommand(ctx), + failover.NewCommand(ctx), helper.NewCommand(ctx), runner.NewCommand(ctx), - sync.NewCommand(ctx), - backup.NewCommand(ctx), ) if err := app.Run(os.Args); err != nil { diff --git a/cmd/redis-tools/pkg/commands/backup/backup.go b/cmd/redis-tools/pkg/commands/backup/backup.go deleted file mode 100644 index 6ef7a80..0000000 --- a/cmd/redis-tools/pkg/commands/backup/backup.go +++ /dev/null @@ -1,191 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backup - -import ( - "context" - "fmt" - "os/exec" - "path" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" - "k8s.io/client-go/kubernetes" -) - -func Backup(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { - appendCommands := c.String("append-commands") - redisPassword := c.String("redis-password") - - instanceName := c.String("redis-name") - clusterIndex := c.String("redis-cluster-index") - podName := instanceName - if clusterIndex == "" { - podName = fmt.Sprintf("rfr-%s-0", instanceName) - } - cliCmd := exec.Command("redis-cli") - logger.Info("backup starting") - if redisPassword != "" { - cliCmd.Args = append(cliCmd.Args, "-a", redisPassword) - } - - if appendCommands != "" { - appendCommandsList := SplitAndTrimSpace(appendCommands) - cliCmd.Args = append(cliCmd.Args, appendCommandsList...) - } - //最后一次bgsave时间 - startLastSaveTimestamp, err := ExecLastSave(cliCmd, podName, logger) - if err != nil { - return err - } - for range make([]struct{}, 300) { - err := ExecBgSave(cliCmd, podName, logger) - if err != nil { - return err - } - time.Sleep(time.Duration(time.Duration.Seconds(5))) - endLastSaveTimestamp, err := ExecLastSave(cliCmd, podName, logger) - if err != nil { - return err - } - // 查看bgsave 有再一次支持 - if endLastSaveTimestamp > startLastSaveTimestamp { - err = KubectlRedisDump(podName, clusterIndex, logger) - if err != nil { - return err - } - break - } - } - - return nil -} - -func ExecLastSave(cliCmd *exec.Cmd, podName string, logger logr.Logger) (string, error) { - lastSaveCmd := &exec.Cmd{} - lastSaveCmd.Args = append(cliCmd.Args, "LASTSAVE") - output, err := KubectlExec(podName, "redis", logger, lastSaveCmd) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - logger.Error(err, string(exitError.Stderr)) - return string(output), err - } - } - logger.Info(string(output)) - return string(output), nil -} - -func ExecBgSave(cliCmd *exec.Cmd, podName string, logger logr.Logger) error { - bgSaveCmd := &exec.Cmd{} - bgSaveCmd.Args = append(cliCmd.Args, "BGSAVE") - output, err := KubectlExec(podName, "redis", logger, bgSaveCmd) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - logger.Error(err, string(exitError.Stderr)) - return err - } - } - logger.Info(string(output)) - return nil -} - -func KubectlExec(podName, containerName string, logger logr.Logger, command *exec.Cmd) (string, error) { - cmd := exec.Command("kubectl", "exec", "-it", podName, "-c", "redis", "--") - cmd.Args = append(cmd.Args, command.Args...) - output, _err := cmd.Output() - if _err != nil { - if exitError, ok := _err.(*exec.ExitError); ok { - logger.Error(_err, string(exitError.Stderr)) - return string(exitError.Stderr), _err - } - } - logger.Info("exc kubectl rdb dump success", "output", string(output)) - return string(output), _err - -} - -func SplitAndTrimSpace(s string) []string { - parts := strings.Split(s, " ") - var nonEmptyParts []string - for _, part := range parts { - trimmedPart := strings.TrimSpace(part) - if trimmedPart != "" { - nonEmptyParts = append(nonEmptyParts, trimmedPart) - } - } - return nonEmptyParts -} - -func KubectlRedisDump(podName, clusterIndex string, logger logr.Logger) error { - logger.Info("redis dump to local", "podName", podName) - backupDir := path.Join("/backup", clusterIndex) - if clusterIndex != "" { - cmd := exec.Command("kubectl", "cp", fmt.Sprintf("%s:%s", podName, "nodes.conf"), path.Join(backupDir, "nodes.conf"), "-c", "redis") - output, _err := cmd.Output() - if _err != nil { - if exitError, ok := _err.(*exec.ExitError); ok { - logger.Error(_err, string(exitError.Stderr)) - } - } else { - logger.Info("exc kubectl nodes.conf dump success", "output", string(output)) - } - } - - cmd := exec.Command("kubectl", "cp", fmt.Sprintf("%s:%s", podName, "dump.rdb"), path.Join(backupDir, "dump.rdb"), "-c", "redis") - output, _err := cmd.Output() - if _err != nil { - if exitError, ok := _err.(*exec.ExitError); ok { - logger.Error(_err, string(exitError.Stderr)) - } - } else { - logger.Info("exc kubectl rdb dump success", "output:", string(output)) - return _err - } - - cmd = exec.Command("kubectl", "cp", fmt.Sprintf("%s:%s", podName, "appendonly.aof"), path.Join(backupDir, "appendonly.aof"), "-c", "redis") - output, _err = cmd.Output() - if _err != nil { - if exitError, ok := _err.(*exec.ExitError); ok { - logger.Error(_err, string(exitError.Stderr)) - return _err - } - } - logger.Info("exc kubectl aof dump success", "output:", string(output)) - return nil -} - -func BgSave(redisPassword, appendCommands, svcName string, logger logr.Logger) { - cmd := exec.Command("redis-cli", "-h", svcName) - if redisPassword != "" { - cmd.Args = append(cmd.Args, "-a", redisPassword) - } - if appendCommands != "" { - appendCommandsList := SplitAndTrimSpace(appendCommands) - cmd.Args = append(cmd.Args, appendCommandsList...) - } - cmd.Args = append(cmd.Args, "LASTSAVE") - output, _err := cmd.Output() - if _err != nil { - if exitError, ok := _err.(*exec.ExitError); ok { - logger.Error(_err, string(exitError.Stderr)) - } - } - logger.Info(string(output)) - -} diff --git a/cmd/redis-tools/pkg/commands/backup/command.go b/cmd/redis-tools/pkg/commands/backup/command.go deleted file mode 100644 index 093a665..0000000 --- a/cmd/redis-tools/pkg/commands/backup/command.go +++ /dev/null @@ -1,273 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backup - -import ( - "context" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/kubernetes/client" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/logger" - "github.com/urfave/cli/v2" -) - -func NewCommand(ctx context.Context) *cli.Command { - return &cli.Command{ - Name: "backup", - Usage: "backup commands", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "namespace", - Usage: "Namespace of current pod", - EnvVars: []string{"NAMESPACE"}, - }, - &cli.StringFlag{ - Name: "pod-name", - Usage: "The name of current pod", - EnvVars: []string{"POD_NAME"}, - }, - &cli.StringFlag{ - Name: "pod-uid", - Usage: "The id of current pod", - EnvVars: []string{"POD_UID"}, - }, - &cli.StringFlag{ - Name: "append-commands", - Usage: "APPEND_COMMANDS for redis-cli", - EnvVars: []string{"APPEND_COMMANDS"}, - }, - &cli.StringFlag{ - Name: "redis-password", - Usage: "redis-password for redis-cli", - EnvVars: []string{"REDIS_PASSWORD"}, - }, - &cli.StringFlag{ - Name: "redis-name", - Usage: "redis-name for redis-cli", - EnvVars: []string{"REDIS_NAME"}, - }, - &cli.StringFlag{ - Name: "redis-cluster-index", - Usage: "redis shard index for backup", - EnvVars: []string{"REDIS_ClUSTER_INDEX"}, - }, - &cli.StringFlag{ - Name: "redis-backup-image", - Usage: "backup image", - EnvVars: []string{"BACKUP_IMAGE"}, - }, - &cli.StringFlag{ - Name: "redis-failover-name", - Usage: "redis sentinel name for backup", - EnvVars: []string{"REDIS_FAILOVER_NAME"}, - }, - &cli.StringFlag{ - Name: "redis-storage-class-name", - Usage: "STORAGE_CLASS_NAME", - EnvVars: []string{"STORAGE_CLASS_NAME"}, - }, - &cli.StringFlag{ - Name: "redis-storage-size", - Usage: "STORAGE_CLASS_NAME", - EnvVars: []string{"STORAGE_SIZE"}, - }, - &cli.StringFlag{ - Name: "redis-schedule-name", - Usage: "SCHEDULE_NAME", - EnvVars: []string{"SCHEDULE_NAME"}, - }, - &cli.StringFlag{ - Name: "redis-cluster-name", - Usage: "REDIS_CLUSTER_NAME", - EnvVars: []string{"REDIS_CLUSTER_NAME"}, - }, - &cli.StringFlag{ - Name: "redis-after-deletion", - Usage: "KEEP_AFTER_DELETION", - EnvVars: []string{"KEEP_AFTER_DELETION"}, - }, - &cli.StringFlag{ - Name: "redis-backup-job-name", - Usage: "", - EnvVars: []string{"BACKUP_JOB_NAME"}, - }, - &cli.StringFlag{ - Name: "s3-endpoint", - Usage: "S3_ENDPOINT", - EnvVars: []string{"S3_ENDPOINT"}, - }, - &cli.StringFlag{ - EnvVars: []string{"S3_REGION"}, - Name: "s3-region", - Usage: "S3_REGION", - }, - &cli.StringFlag{ - EnvVars: []string{"DATA_DIR"}, - Name: "data-dir", - Usage: "DATA_DIR", - }, - &cli.StringFlag{ - EnvVars: []string{"S3_OBJECT_DIR"}, - Name: "s3-object-dir", - Usage: "S3_OBJECT_DIR", - }, - &cli.StringFlag{ - EnvVars: []string{"S3_BUCKET_NAME"}, - Name: "s3-bucket-name", - Usage: "S3_BUCKET_NAME", - }, - &cli.StringFlag{ - Name: "s3-object-name", - Usage: "S3_OBJECT_NAME", - EnvVars: []string{"S3_OBJECT_NAME"}, - }, - &cli.StringFlag{ - Name: "s3-secret", - Usage: "S3_SECRET", - EnvVars: []string{"S3_SECRET"}, - }, - &cli.StringFlag{ - Name: "backoff-limit", - Usage: "BACKOFF_LIMIT", - EnvVars: []string{"BACKOFF_LIMIT"}, - }, - }, - Subcommands: []*cli.Command{ - { - Name: "backup", - Usage: "backup [option]", - Flags: []cli.Flag{}, - Action: func(c *cli.Context) error { - - logger := logger.NewLogger(c).WithName("backup") - - client, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - err = Backup(ctx, c, client, logger) - if err != nil { - logger.Error(err, "backup, error") - return cli.Exit(err, 1) - } - return nil - }, - }, - { - Name: "restore", - Usage: "restore [options]", - Description: "restore", - Flags: []cli.Flag{}, - Action: func(c *cli.Context) error { - logger := logger.NewLogger(c).WithName("Restore") - err := Restore(ctx, c, logger) - if err != nil { - logger.Error(err, "backup, error") - return cli.Exit(err, 1) - } - return nil - }, - }, - { - Name: "schedule", - Usage: "schedule [options]", - Description: "schedule", - Flags: []cli.Flag{}, - Action: func(c *cli.Context) error { - logger := logger.NewLogger(c).WithName("Schedule") - kubenetesClient, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - - err = ScheduleCreateRedisBackup(ctx, c, kubenetesClient, logger) - if err != nil { - logger.Error(err, "schedule, error") - return cli.Exit(err, 1) - } - - return nil - }, - }, - { - Name: "pull", - Usage: "pull [options]", - Description: "pull", - Flags: []cli.Flag{}, - Action: func(c *cli.Context) error { - logger := logger.NewLogger(c).WithName("pull") - kubenetesClient, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - - err = Pull(ctx, c, kubenetesClient, logger) - if err != nil { - logger.Error(err, "pull, error") - return cli.Exit(err, 1) - } - - return nil - }, - }, - { - Name: "push", - Usage: "push [options]", - Description: "push", - Flags: []cli.Flag{}, - Action: func(c *cli.Context) error { - logger := logger.NewLogger(c).WithName("push") - kubenetesClient, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - - err = PushFile2S3(ctx, c, kubenetesClient, logger) - if err != nil { - logger.Error(err, "push, error") - return cli.Exit(err, 1) - } - return nil - }, - }, - { - Name: "rename", - Usage: "rename [options]", - Description: "rename", - Flags: []cli.Flag{}, - Action: func(c *cli.Context) error { - logger := logger.NewLogger(c).WithName("rename") - kubenetesClient, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - - err = RenameCluster(ctx, c, kubenetesClient, logger) - if err != nil { - logger.Error(err, "rename, error") - return cli.Exit(err, 1) - } - return nil - }, - }, - }, - } -} diff --git a/cmd/redis-tools/pkg/commands/backup/pull.go b/cmd/redis-tools/pkg/commands/backup/pull.go deleted file mode 100644 index 0374bd9..0000000 --- a/cmd/redis-tools/pkg/commands/backup/pull.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backup - -import ( - "context" - "fmt" - "io" - "log" - "net/url" - "os" - "os/exec" - "strings" - - "github.com/go-logr/logr" - minio "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - "github.com/urfave/cli/v2" - "k8s.io/client-go/kubernetes" -) - -func downloadRdbFromS3(targetFile string) { - secureFlag := false - s3_url := os.Getenv("S3_ENDPOINT") - endpoint, err := url.Parse(s3_url) - if err != nil { - log.Fatalln(err) - } - if endpoint.Scheme == "https" { - secureFlag = true - } - s3Client, err := minio.New(endpoint.Host, &minio.Options{ - Creds: credentials.NewStaticV4(ReadFileToString("AWS_ACCESS_KEY_ID"), ReadFileToString("AWS_SECRET_ACCESS_KEY"), ReadFileToString("S3_TOKEN")), - Region: os.Getenv("S3_REGION"), - Secure: secureFlag, - }) - if err != nil { - log.Fatalln(err) - } - reader, err := s3Client.GetObject(context.Background(), os.Getenv("S3_BUCKET_NAME"), os.Getenv("S3_OBJECT_NAME"), minio.GetObjectOptions{}) - if err != nil { - log.Fatalln(err) - } - defer reader.Close() - localFile, err := os.Create(targetFile) - if err != nil { - log.Fatalln(err) - } - defer localFile.Close() - - stat, err := reader.Stat() - if err != nil { - log.Fatalln(err) - } - - if _, err := io.CopyN(localFile, reader, stat.Size); err != nil { - log.Fatalln(err) - } - log.Printf("File %s download success!", targetFile) - -} - -func checkRdb(targetFile string) (string, error) { - if os.Getenv("RDB_CHECK") == "true" { - out, err := exec.Command("redis-check-rdb", targetFile).Output() - if err == nil && strings.Contains(string(out), "RDB ERROR DETECTED") { - return string(out), fmt.Errorf("rdb check err") - } - return string(out), err - } - return "", nil -} - -func Pull(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { - s3ObjectName := c.String("s3-object-name") - // Prevent auto-generation of slots by the RDB during restoration to avoid slot confusion. - if getPodIndex() != "0" && strings.Contains(s3ObjectName, "redis-cluster") { - logger.Info("skip down cluster nodes when restore") - return nil - } - TARGET_FILE := os.Getenv("TARGET_FILE") - logger.Info(fmt.Sprintf("TARGET_FILE: %s", TARGET_FILE)) - if _, err := os.Stat(TARGET_FILE); err == nil { - logger.Info(fmt.Sprintf("TARGET_FILE: %s exists", TARGET_FILE)) - out, _err := checkRdb(TARGET_FILE) - logger.Info(fmt.Sprintf("check Rdb out : %s", out)) - if _err != nil { - logger.Info("check rdb fail!") - return _err - } - if os.Getenv("REWRITE") != "false" { - logger.Info("skip re download rdb file") - return nil - } - } - //if no exists or rdb check err,re download - if os.Getenv("S3_ENDPOINT") != "" { - downloadRdbFromS3(TARGET_FILE) - } - out, _err := checkRdb(TARGET_FILE) - logger.Info(fmt.Sprintf("check Rdb out : %s", out)) - if _err != nil { - logger.Info("check rdb fail!") - return _err - } - return nil -} diff --git a/cmd/redis-tools/pkg/commands/backup/push.go b/cmd/redis-tools/pkg/commands/backup/push.go deleted file mode 100644 index 3c21e64..0000000 --- a/cmd/redis-tools/pkg/commands/backup/push.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backup - -import ( - "context" - "fmt" - "log" - "net/url" - "os" - "path" - - "github.com/go-logr/logr" - minio "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - "github.com/urfave/cli/v2" - "k8s.io/client-go/kubernetes" -) - -func PushFile2S3(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { - secureFlag := false - s3Url := c.String("s3-endpoint") - s3Region := c.String("s3-region") - dataDir := c.String("data-dir") - s3ObjectDir := c.String("s3-object-dir") - s3BucketName := c.String("s3-bucket-name") - if s3Url == "" { - logger.Info("S3_ENDPOINT is empty") - return fmt.Errorf("S3_ENDPOINT is empty") - } - endpoint, err := url.Parse(s3Url) - if err != nil { - logger.Error(err, "S3_ENDPOINT is invalid") - return err - } - if endpoint.Scheme == "https" { - secureFlag = true - } - s3Client, err := minio.New(endpoint.Host, &minio.Options{ - Creds: credentials.NewStaticV4(ReadFileToString("AWS_ACCESS_KEY_ID"), ReadFileToString("AWS_SECRET_ACCESS_KEY"), ReadFileToString("S3_TOKEN")), - Region: s3Region, - Secure: secureFlag, - }) - if err != nil { - logger.Error(err, "S3 client init failed") - return err - } - files, err := os.ReadDir(dataDir) - if err != nil { - logger.Error(err, "Read data dir failed") - return err - } - for _, file := range files { - filepath := path.Join(dataDir, file.Name()) - object, err := os.Open(filepath) - if err != nil { - logger.Error(err, "S3 open file failed") - return err - } - defer object.Close() - objectStat, err := object.Stat() - if err != nil { - logger.Error(err, "S3 get file stat failed") - return err - } - s3Path := path.Join(s3ObjectDir, file.Name()) - n, err := s3Client.PutObject(context.Background(), s3BucketName, s3Path, object, objectStat.Size(), minio.PutObjectOptions{ContentType: "application/octet-stream"}) - if err != nil { - logger.Error(err, "S3 upload file failed") - return err - } - log.Println("Uploaded", s3Path, " of size: ", n.Size, "Successfully.") - } - return nil -} - -func ReadFileToString(filename string) string { - filename = path.Join("/s3_secret", filename) - content, err := os.ReadFile(filename) - if err != nil { - log.Println(err) - return "" - } - str := string(content) - return str -} diff --git a/cmd/redis-tools/pkg/commands/backup/rename.go b/cmd/redis-tools/pkg/commands/backup/rename.go deleted file mode 100644 index c01e885..0000000 --- a/cmd/redis-tools/pkg/commands/backup/rename.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -//This is used to rename the rdb node downloaded from redis-cluster using redis-cli - -package backup - -import ( - "context" - "encoding/json" - "fmt" - "log" - "os" - "path" - "sort" - "strconv" - - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" - "k8s.io/client-go/kubernetes" -) - -type NodeInfo struct { - Name string `json:"name,omitempty"` - Host string `json:"host,omitempty"` - Port int64 `json:"port,omitempty"` - Replicate string `json:"replicate,omitempty"` - SLotsCount int64 `json:"slots_count,omitempty"` - Slots [][]int64 `json:"slots,omitempty"` - Flags string `json:"flags,omitempty"` - CurrentEpoch int64 `json:"current_epoch,omitempty"` -} - -func RenameCluster(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { - dataDir := c.String("data-dir") - filePath := path.Join(dataDir, "nodes.json") - - buff, err := os.ReadFile(filePath) - if err != nil { - logger.Error(err, "read nodes.json err") - return err - } - var dat []NodeInfo - if err := json.Unmarshal(buff, &dat); err != nil { - logger.Error(err, "json.Unmarshal nodes.json err") - return err - } - filtered := []NodeInfo{} - for _, v := range dat { - if v.SLotsCount != 0 { - filtered = append(filtered, v) - } - } - sort.Slice(filtered, func(i, j int) bool { - iLen := len(filtered[i].Slots[len(filtered[i].Slots)-1]) - jLen := len(filtered[j].Slots[len(filtered[j].Slots)-1]) - iLast := filtered[i].Slots[len(filtered[i].Slots)-1][iLen-1] - jLast := filtered[j].Slots[len(filtered[j].Slots)-1][jLen-1] - return iLast < jLast - }) - log.Println("Filtered file len: ", len(filtered)) - for i, node := range filtered { - rdbFilename := fmt.Sprintf("redis-node-%s-%d-%s.rdb", node.Host, node.Port, node.Name) - err := os.Rename(path.Join(dataDir, rdbFilename), path.Join(dataDir, fmt.Sprintf("%d.rdb", i))) - if err != nil { - logger.Error(err, "rename file err", "file", rdbFilename) - return err - } - nodeFile := path.Join(dataDir, fmt.Sprintf("%d.node.conf", i)) - slotStr := "" - for _, slot_range := range node.Slots { - start := strconv.Itoa(int(slot_range[0])) - if len(slot_range) > 1 { - end := strconv.Itoa(int(slot_range[1])) - slotStr += start + "-" + end + " " - } else { - slotStr += start + " " - } - - } - line := fmt.Sprintf(`%s %s:%d@%d myself,%s - 0 0 %d connected %s -vars currentEpoch %d lastVoteEpoch 0`, node.Name, "127.0.0.0", 6379, 16379, node.Flags, node.CurrentEpoch, slotStr, node.CurrentEpoch) - err = os.WriteFile(nodeFile, []byte(line), 0666) - if err != nil { - logger.Error(err, "write node file err", "file", nodeFile) - return err - } - } - return nil -} diff --git a/cmd/redis-tools/pkg/commands/backup/restore.go b/cmd/redis-tools/pkg/commands/backup/restore.go deleted file mode 100644 index a53a051..0000000 --- a/cmd/redis-tools/pkg/commands/backup/restore.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backup - -import ( - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "io" - "os" - "path" - "strings" - - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" -) - -func Restore(ctx context.Context, c *cli.Context, logger logr.Logger) error { - clusterIndex := c.String("redis-cluster-index") - dataDir := "/data" - backupDir := path.Join("/backup", clusterIndex) - - if getPodIndex() != "0" && clusterIndex != "" { - logger.Info("sts's pod index is not 0, cluster skip restore") - return nil - } - - if clusterIndex != "" { - dumpFile := "nodes.conf" - if (!FileExists(path.Join(dataDir, dumpFile))) && FileExists(path.Join(backupDir, dumpFile)) { - content, err := FileToString(path.Join(backupDir, dumpFile)) - if err != nil { - return err - } - slots := ExtractMyselfSlots(content) - node_id, err := GenerateRandomCode(40) - if err != nil { - return err - } - line := fmt.Sprintf(`%s %s:%d@%d %s - 0 0 %d connected %s -vars currentEpoch %d lastVoteEpoch 0`, node_id, "127.0.0.1", 6379, 16379, "myself,master", 1, slots, 1) - logger.Info("nodes info", "line:", line) - err = os.WriteFile(path.Join(dataDir, dumpFile), []byte(line), 0666) - if err != nil { - logger.Error(err, "write node file err") - return err - } - } else { - logger.Info("data dir's node conf rdb exists or skip rdb") - } - } - - dumpFile := "dump.rdb" - if (!FileExists(path.Join(dataDir, dumpFile))) && FileExists(path.Join(backupDir, dumpFile)) { - if err := CopyFile(path.Join(backupDir, dumpFile), path.Join(dataDir, dumpFile), logger); err != nil { - logger.Error(err, "copy rdb err") - } - } else { - logger.Info("data dir's rdb exists or skip rdb") - } - - dumpFile = "appendonly.aof" - if (!FileExists(path.Join(dataDir, dumpFile))) && FileExists(path.Join(backupDir, dumpFile)) { - if err := CopyFile(path.Join(backupDir, dumpFile), path.Join(dataDir, dumpFile), logger); err != nil { - logger.Error(err, "copy aof err") - } - } else { - logger.Info("data dir's aof exists or skip ") - } - return nil -} - -func CopyFile(sourceFile, destinationFile string, logger logr.Logger) error { - // 打开源文件 - src, err := os.Open(sourceFile) - if err != nil { - return err - } - defer src.Close() - - // 创建目标文件 - dst, err := os.Create(destinationFile) - if err != nil { - return err - } - defer dst.Close() - - // 复制文件内容 - _, err = io.Copy(dst, src) - if err != nil { - return err - } - - // 刷新缓冲区,确保文件内容已写入目标文件 - err = dst.Sync() - if err != nil { - return err - } - - logger.Info("File copy success", "sourceFile", sourceFile, "destinationFile", destinationFile) - return nil -} - -func FileExists(filename string) bool { - _, err := os.Stat(filename) - if err == nil { - return true - } else if os.IsNotExist(err) { - return false - } else { - panic(err) - } -} - -func getPodIndex() string { - hostname, err := os.Hostname() - if err != nil { - panic(err) - } - parts := strings.Split(hostname, "-") - return parts[len(parts)-1] - -} - -func GenerateRandomCode(length int) (string, error) { - randomBytes := make([]byte, length/2) - _, err := rand.Read(randomBytes) - if err != nil { - return "", err - } - - randomCode := hex.EncodeToString(randomBytes) - return randomCode, nil -} - -func ExtractMyselfSlots(data string) string { - lines := strings.Split(data, "\n") - for _, line := range lines { - if strings.Contains(line, "myself") { - columns := strings.Fields(line) - if len(columns) >= 9 { - return strings.Join(columns[8:], " ") - } - } - } - return "" -} - -func FileToString(filename string) (string, error) { - content, err := os.ReadFile(filename) - if err != nil { - return "", err - } - - str := string(content) - return str, nil -} diff --git a/cmd/redis-tools/pkg/commands/backup/schedule.go b/cmd/redis-tools/pkg/commands/backup/schedule.go deleted file mode 100644 index 5edc811..0000000 --- a/cmd/redis-tools/pkg/commands/backup/schedule.go +++ /dev/null @@ -1,205 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package backup - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" - "k8s.io/client-go/kubernetes" -) - -func ScheduleCreateRedisBackup(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { - keepAfterDeletion := c.String("redis-after-deletion") - backupJobName := c.String("redis-backup-job-name") - redisClusterName := c.String("redis-cluster-name") - scheduleName := c.String("redis-schedule-name") - s3BucketName := c.String("s3-bucket-name") - backoffLimit := c.String("backoff-limit") - image := c.String("redis-backup-image") - - if image == "" { - image = "''" - } - redisFailoverName := c.String("redis-failover-name") - storageClassName := c.String("redis-storage-class-name") - storage := c.String("redis-storage-size") - s3Secret := c.String("s3-secret") - content := "" - - if redisClusterName != "" { - content = genRedisClusterYAML(keepAfterDeletion, backupJobName, redisClusterName, scheduleName, image, storageClassName, storage, s3BucketName, backoffLimit, s3Secret) - } - if redisFailoverName != "" { - content = genFailoverYAML(keepAfterDeletion, backupJobName, redisFailoverName, scheduleName, image, storageClassName, storage, s3BucketName, backoffLimit, s3Secret) - } - logger.Info("Gen success", "content", content) - cmd := exec.Command("kubectl", "apply", "-f", "-") - cmd.Stdin = strings.NewReader(content) - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - logger.Error(err, string(exitError.Stderr)) - return err - } - } - return nil -} - -func genRedisClusterYAML(keepAfterDeletion, backupJobName, redisClusterName, scheduleName, image, storageClassName, storage, s3BucketName, backoffLimit, s3Secret string) string { - var yamlContent string - if s3BucketName != "" { - currentTime := time.Now().UTC() - s3Dir := fmt.Sprintf("data/backup/redis-cluster/schedule/%s", currentTime.Format("2006-01-02T15:04:05Z")) - - yamlContent = fmt.Sprintf(`apiVersion: redis.middleware.alauda.io/v1 -kind: RedisClusterBackup -metadata: - name: %s - annotations: - createType: auto - labels: - redis.kun/name: %s - redis.kun/scheduleName: %s -spec: - backoffLimit: %s - image: %s - source: - redisClusterName: %s - storage: %s - target: - s3Option: - bucket: %s - dir: %s - s3Secret: %s`, backupJobName, redisClusterName, scheduleName, backoffLimit, image, redisClusterName, storage, s3BucketName, s3Dir, s3Secret) - } else if keepAfterDeletion == "true" { - yamlContent = fmt.Sprintf(`apiVersion: redis.middleware.alauda.io/v1 -kind: RedisClusterBackup -metadata: - annotations: - createType: auto - name: %s - labels: - redis.kun/name: %s - redis.kun/scheduleName: %s -spec: - image: %s - source: - redisClusterName: %s - storageClassName: %s - storage: %s`, backupJobName, redisClusterName, scheduleName, image, redisClusterName, storageClassName, storage) - } else { - backupJobUID := "example-backup-job-uid" // Set the value based on your requirement - - yamlContent = fmt.Sprintf(`apiVersion: redis.middleware.alauda.io/v1 -kind: RedisClusterBackup -metadata: - annotations: - createType: auto - name: %s - ownerReferences: - - apiVersion: v1 - kind: Pod - name: %s - uid: %s - labels: - redis.kun/name: %s - redis.kun/scheduleName: %s -spec: - image: %s - source: - redisClusterName: %s - storageClassName: %s - storage: %s`, backupJobName, backupJobName, backupJobUID, redisClusterName, scheduleName, image, redisClusterName, storageClassName, storage) - } - return yamlContent -} - -func genFailoverYAML(keepAfterDeletion, backupJobName, redisFailoverName, scheduleName, image, storageClassName, storage, s3BucketName, backoffLimit, s3Secret string) string { - var yamlContent string - if s3BucketName != "" { - currentTime := time.Now().UTC() - s3Dir := fmt.Sprintf("data/backup/redis-sentinel/schedule/%s", currentTime.Format("2006-01-02T15:04:05Z")) - yamlContent = fmt.Sprintf(`apiVersion: redis.middleware.alauda.io/v1 -kind: RedisBackup -metadata: - annotations: - createType: auto - name: %s - labels: - redisfailovers.databases.spotahome.com/name: %s - redisfailovers.databases.spotahome.com/scheduleName: %s -spec: - backoffLimit: %s - image: %s - source: - redisName: %s - storage: %s - target: - s3Option: - bucket: %s - dir: %s - s3Secret: %s`, backupJobName, redisFailoverName, scheduleName, backoffLimit, image, redisFailoverName, storage, s3BucketName, s3Dir, s3Secret) - } else if keepAfterDeletion == "true" { - yamlContent = fmt.Sprintf(`apiVersion: redis.middleware.alauda.io/v1 -kind: RedisBackup -metadata: - annotations: - createType: auto - name: %s - labels: - redisfailovers.databases.spotahome.com/name: %s - redisfailovers.databases.spotahome.com/scheduleName: %s -spec: - image: %s - source: - redisFailoverName: %s - storageClassName: %s - storage: %s`, backupJobName, redisFailoverName, scheduleName, image, redisFailoverName, storageClassName, storage) - } else { - backupJobUID := "example-backup-job-uid" // Set the value based on your requirement - - yamlContent = fmt.Sprintf(`apiVersion: redis.middleware.alauda.io/v1 -kind: RedisBackup -metadata: - annotations: - createType: auto - name: %s - ownerReferences: - - apiVersion: v1 - kind: Pod - name: %s - uid: %s - labels: - redisfailovers.databases.spotahome.com/name: %s - redisfailovers.databases.spotahome.com/scheduleName: %s -spec: - image: %s - source: - redisFailoverName: %s - storageClassName: %s - storage: %s`, backupJobName, backupJobName, backupJobUID, redisFailoverName, scheduleName, image, redisFailoverName, storageClassName, storage) - } - return yamlContent -} diff --git a/cmd/redis-tools/pkg/commands/cluster/expose.go b/cmd/redis-tools/pkg/commands/cluster/expose.go deleted file mode 100644 index a0ac3b0..0000000 --- a/cmd/redis-tools/pkg/commands/cluster/expose.go +++ /dev/null @@ -1,273 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cluster - -import ( - "context" - "fmt" - "net/netip" - "os" - "strconv" - "strings" - "time" - - "github.com/go-logr/logr" - appv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/util/retry" -) - -// RetryGet -func RetryGet(f func() error, steps ...int) error { - step := 5 - if len(steps) > 0 && steps[0] > 0 { - step = steps[0] - } - return retry.OnError(wait.Backoff{ - Steps: step, - Duration: 400 * time.Millisecond, - Factor: 2.0, - Jitter: 2, - }, func(err error) bool { - return errors.IsInternalError(err) || errors.IsServerTimeout(err) || errors.IsServiceUnavailable(err) || - errors.IsTimeout(err) || errors.IsTooManyRequests(err) - }, f) -} - -// GetStatefulSet -func GetStatefulSet(ctx context.Context, client *kubernetes.Clientset, namespace, name string) (*appv1.StatefulSet, error) { - var sts *appv1.StatefulSet - if err := RetryGet(func() (err error) { - sts, err = client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) - return err - }); err != nil { - return nil, err - } - return sts, nil -} - -// GetPod -func GetPod(ctx context.Context, client *kubernetes.Clientset, namespace, name string, logger logr.Logger) (*corev1.Pod, error) { - var pod *corev1.Pod - if err := RetryGet(func() (err error) { - logger.Info("get pods ip", "namespace", namespace, "name", name) - if pod, err = client.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}); err != nil { - logger.Error(err, "get pods failed") - return err - } else if pod.Status.PodIP == "" { - return errors.NewTimeoutError("pod have not assigied pod ip", 0) - } else if pod.Status.HostIP == "" { - return errors.NewTimeoutError("pod have not assigied host ip", 0) - } - return - }, 20); err != nil { - return nil, err - } - return pod, nil -} - -// ExposeNodePort -func ExposeNodePort(ctx context.Context, client *kubernetes.Clientset, namespace, podName, ipfamily string, nodeportEnabled bool, customPort bool, logger logr.Logger) error { - logger.Info("Info", "nodeport", nodeportEnabled, "ipfamily", ipfamily) - pod, err := GetPod(ctx, client, namespace, podName, logger) - if err != nil { - logger.Error(err, "get pods failed", "namespace", namespace, "name", podName) - return err - } - if pod.Status.HostIP == "" { - return fmt.Errorf("pod not found or pod with invalid hostIP") - } - - index := strings.LastIndex(podName, "-") - sts, err := GetStatefulSet(ctx, client, namespace, podName[0:index]) - if err != nil { - return err - } - - var ( - announceIp = pod.Status.PodIP - announcePort int32 = 6379 - // announceIPort is this port necessary ? - announceIPort int32 = 16379 - ) - if nodeportEnabled { - // get svc - podSvc, err := RetryGetService(client, namespace, podName, true, 1) - if customPort { - podSvc, err = RetryGetService(client, namespace, podName, true, 20) - } - if errors.IsNotFound(err) { - newSvc := newPodService(namespace, podName, pod.GetLabels(), sts.OwnerReferences, ipfamily) - if podSvc, err = client.CoreV1().Services(namespace).Create(ctx, newSvc, metav1.CreateOptions{}); err != nil { - logger.Error(err, "create pod service failed") - return err - } - } else if err != nil { - logger.Error(err, "get service failed", "target", fmt.Sprintf("%s/%s", namespace, podName)) - return err - } - for _, v := range podSvc.Spec.Ports { - if v.Name == "client" { - announcePort = v.NodePort - } - if v.Name == "gossip" { - announceIPort = v.NodePort - } - } - - node, err := client.CoreV1().Nodes().Get(context.TODO(), pod.Spec.NodeName, metav1.GetOptions{}) - if err != nil { - logger.Error(err, "get nodes err", "node", node.Name) - return err - } - logger.Info("get nodes success", "Name", node.Name) - for _, addr := range node.Status.Addresses { - if addr.Type == corev1.NodeExternalIP || addr.Type == corev1.NodeInternalIP { - if addr.Address == "" { - continue - } - logger.Info("Parsing node ", "IP", addr.Address) - ip, err := netip.ParseAddr(addr.Address) - if err != nil { - logger.Error(err, "parse address err", "address", addr.Address) - return err - } - if ipfamily == "IPv6" && ip.Is6() { - announceIp = addr.Address - break - } else if ipfamily != "IPv6" && ip.Is4() { - announceIp = addr.Address - break - } - } - } - } else { - for _, addr := range pod.Status.PodIPs { - ip, err := netip.ParseAddr(addr.IP) - if err != nil { - return err - } - if ipfamily == "IPv6" && ip.Is6() { - announceIp = addr.IP - break - } else if ipfamily != "IPv6" && ip.Is4() { - announceIp = addr.IP - break - } - } - } - - format_announceIp := strings.Replace(announceIp, ":", "-", -1) - labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s"},{"op":"add","path":"/metadata/labels/%s","value":"%s"},{"op":"add","path":"/metadata/labels/%s","value":"%s"}]`, - strings.Replace("middleware.alauda.io/announce_ip", "/", "~1", -1), format_announceIp, - strings.Replace("middleware.alauda.io/announce_port", "/", "~1", -1), strconv.Itoa(int(announcePort)), - strings.Replace("middleware.alauda.io/announce_iport", "/", "~1", -1), strconv.Itoa(int(announceIPort))) - - logger.Info(labelPatch) - _, err = client.CoreV1().Pods(pod.Namespace).Patch(ctx, podName, types.JSONPatchType, []byte(labelPatch), metav1.PatchOptions{}) - if err != nil { - logger.Error(err, "patch pod label failed") - return err - } - configContent := fmt.Sprintf(`cluster-announce-ip %s -cluster-announce-port %d -cluster-announce-bus-port %d`, announceIp, announcePort, announceIPort) - - return os.WriteFile("/data/announce.conf", []byte(configContent), 0644) -} - -func newPodService(namespace, name string, labels map[string]string, ownerRef []metav1.OwnerReference, ipfamily string) *corev1.Service { - ptype := corev1.IPFamilyPolicySingleStack - protocol := []corev1.IPFamily{} - if ipfamily == "IPv6" { - protocol = append(protocol, corev1.IPv6Protocol) - } else { - protocol = append(protocol, corev1.IPv4Protocol) - } - - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: ownerRef, - }, - Spec: corev1.ServiceSpec{ - Selector: map[string]string{"statefulset.kubernetes.io/pod-name": name}, - Type: corev1.ServiceTypeNodePort, - IPFamilies: protocol, - IPFamilyPolicy: &ptype, - Ports: []corev1.ServicePort{ - { - Name: "client", - Port: 6379, - TargetPort: intstr.FromInt(6379), - Protocol: "TCP", - }, - { - Name: "gossip", - Port: 16379, - TargetPort: intstr.FromInt(16379), - Protocol: "TCP", - }, - }, - }, - } -} - -func RetryGetService(clientset *kubernetes.Clientset, svcNamespace, svcName string, isCluster bool, count int) (*corev1.Service, error) { - fmt.Println("RetryGetService", "NS", svcNamespace, "Name", svcName, "count", count) - svc, err := clientset.CoreV1().Services(svcNamespace).Get(context.TODO(), svcName, metav1.GetOptions{}) - if count <= 1 { - return svc, err - } - if errors.IsNotFound(err) { - fmt.Println("waiting for svc create") - time.Sleep(time.Second * 5) - svc, err = RetryGetService(clientset, svcNamespace, svcName, isCluster, count-1) - } - if err == nil && isCluster { - if len(svc.Spec.Ports) < 2 { - fmt.Println("waiting for svc bus port update") - time.Sleep(time.Second * 3) - svc, err = RetryGetService(clientset, svcNamespace, svcName, isCluster, count-1) - } else { - for _, port := range svc.Spec.Ports { - if port.NodePort == 0 { - time.Sleep(time.Second * 3) - fmt.Println("wait for node port allocation") - svc, err = RetryGetService(clientset, svcNamespace, svcName, isCluster, count-1) - } - } - } - - } - return svc, err -} - -func DefaultOwnerReferences(pod *corev1.Pod) []metav1.OwnerReference { - or := metav1.NewControllerRef(&pod.ObjectMeta, corev1.SchemeGroupVersion.WithKind("Pod")) - or.BlockOwnerDeletion = nil - or.Controller = nil - return []metav1.OwnerReference{*or} -} diff --git a/cmd/redis-tools/pkg/commands/cluster/heal.go b/cmd/redis-tools/pkg/commands/cluster/heal.go deleted file mode 100644 index 0326722..0000000 --- a/cmd/redis-tools/pkg/commands/cluster/heal.go +++ /dev/null @@ -1,424 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cluster - -import ( - "context" - "crypto/tls" - "fmt" - "net" - "os" - "path" - "strings" - "time" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -var ( - IsPersistentEnabled = (os.Getenv("PERSISTENT_ENABLED") == "true") - IsNodePortEnaled = (os.Getenv("NODEPORT_ENABLED") == "true") - IsTLSEnabled = (os.Getenv("TLS_ENABLED") == "true") -) - -const ( - operatorPasswordMountPath = "/account/password" - injectedPasswordPath = "/tmp/newpass" -) - -// Heal heal may fail when updated password for redis 4,5 -func Heal(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { - podName := c.String("pod-name") - - logger.Info(fmt.Sprintf("check cluster status before pod %s startup", podName)) - nodes, err := getClusterNodes(c, ctx, client, true, logger) - if err != nil { - logger.Error(err, "update nodes.conf with unknown error") - return err - } - if nodes == nil { - logger.Info("no nodes found") - return nil - } - nodesInfo, _ := nodes.Marshal() - logger.Info("get nodes", "nodes", string(nodesInfo)) - - var ( - self = nodes.Self() - replicas = nodes.Replicas(self.ID) - ) - if !self.IsJoined() || len(replicas) == 0 { - return nil - } - - authInfo, err := getRedisAuthInfo(c) - if err != nil { - logger.Error(err, "load redis operator user info failed") - return err - } - - masterExists := false - // NOTE: when node is in importing state, if do force failover - // some slots will missing, or there will be multi master in cluster - if slots := self.Slots(); !slots.IsImporting() { - pods, _ := getPodsOfShard(ctx, c, client, logger) - for _, pod := range pods { - logger.Info(fmt.Sprintf("check pod %s", pod.GetName())) - if pod.GetName() == podName { - continue - } - if pod.GetDeletionTimestamp() != nil { - continue - } - - if err := func() error { - addr := net.JoinHostPort(pod.Status.PodIP, "6379") - announceIP := strings.ReplaceAll(pod.Labels["middleware.alauda.io/announce_ip"], "-", ":") - announcePort := pod.Labels["middleware.alauda.io/announce_port"] - if announceIP != "" && announcePort != "" { - addr = net.JoinHostPort(announceIP, announcePort) - } - logger.Info("connect to redis", "addr", addr) - redisClient := redis.NewClient(addr, *authInfo) - defer redisClient.Close() - - nodes, err := redisClient.Nodes(ctx) - if err != nil { - logger.Error(err, "get nodes info failed") - return err - } - currentNode := nodes.Self() - if !currentNode.IsJoined() { - logger.Info("unjoined node") - return fmt.Errorf("unjoined node") - } - if currentNode.Role == redis.MasterRole { - // this shard has got one new master - // clean and start - logger.Info("master nodes exists") - masterExists = true - return nil - } - - currentMaster := nodes.Get(currentNode.MasterID) - if currentMaster != nil && (currentMaster.ID != self.ID && - !strings.Contains(currentMaster.Flags, "fail") && - !strings.Contains(currentMaster.Flags, "noaddr")) { - masterExists = true - return nil - } - if err := doRedisFailover(ctx, redisClient, ForceFailoverAction, logger); err != nil { - return err - } - return nil - }(); err != nil { - continue - } - break - } - } - - if masterExists { - // check current rdb and aof - var ( - rdbFile = "/data/dump.rdb" - aofFile = "/data/appendonly.aof" - oldestModTime = time.Now().Add(time.Second * -3600) - ) - if info, _ := os.Stat(rdbFile); info != nil { - if info.ModTime().Before(oldestModTime) { - // clean this file - logger.Info("clean old dump.rdb") - os.Remove(rdbFile) - } - } - logger.Info("clean appendonly.aof") - os.Remove(aofFile) - } - return nil -} - -type FailoverAction string - -const ( - NoFailoverAction FailoverAction = "" - ForceFailoverAction FailoverAction = "FORCE" - TakeoverFailoverAction FailoverAction = "TAKEOVER" -) - -func doRedisFailover(ctx context.Context, cli redis.Client, action FailoverAction, logger logr.Logger) (err error) { - args := []interface{}{"FAILOVER"} - if action != "" { - args = append(args, action) - } - if _, err := cli.Do(ctx, "CLUSTER", args...); err != nil { - logger.Error(err, "do failover failed", "action", action) - return err - } - - logger.Info("wait 5s for failover") - time.Sleep(time.Second * 5) - - nodes, err := cli.Nodes(ctx) - if err != nil { - logger.Error(err, "fetch cluster nodes failed") - return err - } - self := nodes.Self() - if self == nil { - return fmt.Errorf("get nodes info failed, as if the nodes.conf is broken") - } - if self.Role == redis.MasterRole { - logger.Info("failover succeed") - return nil - } - if action == ForceFailoverAction { - return doRedisFailover(ctx, cli, TakeoverFailoverAction, logger) - } - return fmt.Errorf("do manual failover failed") -} - -func getPodsOfShard(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) (pods []corev1.Pod, err error) { - var ( - namespace = c.String("namespace") - podName = c.String("pod-name") - ) - if podName == "" { - podName, _ = os.Hostname() - } - if podName == "" { - return nil, nil - } - splitIndex := strings.LastIndex(podName, "-") - stsName := podName[0:splitIndex] - - labels := map[string]string{ - "middleware.instance/type": "distributed-redis-cluster", - "statefulSet": stsName, - } - - if err = RetryGet(func() error { - if resp, err := client.CoreV1().Pods(namespace).List(ctx, v1.ListOptions{ - LabelSelector: v1.FormatLabelSelector(&v1.LabelSelector{MatchLabels: labels}), - Limit: 5, - }); err != nil { - return err - } else { - pods = resp.Items - } - return nil - }, 3); err != nil { - logger.Error(err, "list statefulset pods failed") - return nil, err - } - return -} - -func getRedisAuthInfo(c *cli.Context) (*redis.AuthInfo, error) { - var ( - operatorName = c.String("operator-username") - passwordPath = operatorPasswordMountPath - isTLSEnabled = c.Bool("tls") - tlsKeyFile = c.String("tls-key-file") - tlsCertFile = c.String("tls-cert-file") - ) - - info := redis.AuthInfo{} - info.Username = operatorName - if operatorName == "" || operatorName == "default" { - if _, err := os.Stat(injectedPasswordPath); err == nil { - passwordPath = injectedPasswordPath - } - } - if data, err := os.ReadFile(passwordPath); err != nil && !os.IsNotExist(err) { - return nil, err - } else { - info.Password = strings.TrimSpace(string(data)) - } - - if !isTLSEnabled { - return &info, nil - } - if tlsKeyFile == "" || tlsCertFile == "" { - return nil, fmt.Errorf("require tls key and cert") - } - - cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) - if err != nil { - return nil, err - } - info.TLSConf = &tls.Config{ - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: true, - } - return &info, nil -} - -func getClusterNodes(c *cli.Context, ctx context.Context, client *kubernetes.Clientset, overwrite bool, logger logr.Logger) (redis.Nodes, error) { - var ( - namespace = c.String("namespace") - podName = c.String("pod-name") - workspace = c.String("workspace") - target = c.String("node-config-name") - prefix = c.String("prefix") - ) - if namespace == "" { - return nil, fmt.Errorf("require namespace") - } - if podName == "" { - return nil, fmt.Errorf("require podname") - } - if workspace == "" { - workspace = "/data" - } - if target == "" { - target = "nodes.conf" - } - - nodeFile := path.Join(workspace, target) - data, err := os.ReadFile(nodeFile) - if err != nil { - if os.IsNotExist(err) { - // sync configmap to local - configName := strings.Join([]string{strings.TrimSuffix(prefix, "-"), podName}, "-") - if data, err = SyncToLocal(ctx, client, namespace, configName, workspace, target, logger); err != nil { - logger.Error(err, "sync nodes.conf from configmap to local failed") - return nil, err - } - } else { - logger.Error(err, "get nodes.conf failed") - return nil, err - } - } - - // new node startup - if len(data) == 0 { - return nil, nil - } - - var ( - lines []string - epochLine string - ) - for _, line := range strings.Split(string(data), "\n") { - line = strings.TrimSpace(line) - if strings.Contains(line, "vars") { - if epochLine == "" { - epochLine = line - } - continue - } - fields := strings.Fields(line) - // NOTE: here ignore the wrong line, this may caused by redis-server crash - if !strings.Contains(line, "connect") || len(fields) < 8 { - continue - } - lines = append(lines, line) - } - if epochLine != "" { - // format: vars currentEpoch 105 lastVoteEpoch 105 - fields := strings.Fields(epochLine) - if len(fields) == 5 { - lines = append(lines, epochLine) - } else if len(fields) < 5 { - if len(fields) == 4 && fields[3] == "lastVoteEpoch" { - fields = append(fields, "0") - lines = append(lines, strings.Join(fields, " ")) - } else if len(fields) == 3 && fields[1] == "currentEpoch" { - fields = append(fields, "lastVoteEpoch", "0") - lines = append(lines, strings.Join(fields, " ")) - } - } - } - - newData := strings.Join(lines, "\n") - nodes := redis.ParseNodes(newData) - if overwrite { - nodeFileBak := nodeFile + ".bak" - if nodes.Self() == nil { - // nodes.conf error with self node id which is not irreparable, remove this node as new node - if err := os.Rename(nodeFile, nodeFileBak); err != nil { - logger.Error(err, "remove nodes.conf failed") - } - logger.Info("no self node record found, clean this node as new node") - return nil, nil - } else if newData != string(data) { - // back the nodes.conf to nodes.conf.bak - _ = os.Rename(nodeFile, nodeFileBak) - - tmpFile := path.Join(workspace, "tmp-"+target) - if err := os.WriteFile(tmpFile, []byte(newData), 0644); err != nil { - logger.Error(err, "update nodes.conf failed") - } else if err := os.Rename(tmpFile, nodeFile); err != nil { - logger.Error(err, "rename tmp-nodes.conf to nodes.conf failed") - } - } - } - return nodes, nil -} - -/* -func getBindableAddresses(logger logr.Logger) []string { - var bindAddrs []string - // generate ip list for startup - if addrs, err := net.InterfaceAddrs(); err != nil { - logger.Error(err, "load ips of container failed") - - if ips := os.Getenv("POD_IPS"); ips != "" { - bindAddrs = strings.Split(ips, ",") - } else if podIp := os.Getenv("POD_IP"); podIp != "" { - bindAddrs = append(bindAddrs, podIp) - } - } else { - for _, addr := range addrs { - switch v := addr.(type) { - case *net.IPNet: - if v.IP.IsLoopback() { - continue - } - bindAddrs = append(bindAddrs, v.IP.String()) - default: - logger.Info("WARNING: unsupported interface %s", addr.String()) - } - } - } - - foundIPv6 := false - foundIPv4 := false - for _, addr := range bindAddrs { - if strings.Contains(addr, ":") { - foundIPv6 = true - } else { - foundIPv4 = true - } - } - - // make sure 127.0.0.1 and ::1 at the end of ip list - if foundIPv4 { - bindAddrs = append(bindAddrs, "127.0.0.1") - } - if foundIPv6 { - bindAddrs = append(bindAddrs, "::1") - } - return bindAddrs -} -*/ diff --git a/cmd/redis-tools/pkg/commands/cluster/proxy_check.go b/cmd/redis-tools/pkg/commands/cluster/proxy_check.go deleted file mode 100644 index 1fac2fe..0000000 --- a/cmd/redis-tools/pkg/commands/cluster/proxy_check.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cluster - -import ( - "context" - "errors" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" -) - -func Check(ctx context.Context, addr string, authInfo redis.AuthInfo) error { - client := redis.NewClient(addr, authInfo) - defer client.Close() - if client == nil { - return errors.New("client is nil") - } - err := client.CheckProxyInfo(ctx) - if err != nil { - return err - } - return nil -} diff --git a/cmd/redis-tools/pkg/commands/cluster/shutdown.go b/cmd/redis-tools/pkg/commands/cluster/shutdown.go deleted file mode 100644 index b353cbe..0000000 --- a/cmd/redis-tools/pkg/commands/cluster/shutdown.go +++ /dev/null @@ -1,181 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cluster - -import ( - "context" - "errors" - "fmt" - "io" - "math/rand" - "net" - "net/netip" - "time" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" - v1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" -) - -// Shutdown 在退出时做 failover -// -// NOTE: 在4.0, 5.0中,在更新密码时,会重启实例。但是由于密码在重启前已经热更新,导致其他脚本无法连接到实例,包括shutdown脚本 -// 为了解决这个问题,针对4,5 版本,会在重启前,先做failover,将master failover 到-0 节点。 -// 由于重启是逆序的,最后一个pod启动成功之后,会使用新密码连接到 master,从而确保服务一直可用,切数据不会丢失 -func Shutdown(ctx context.Context, c *cli.Context, client *kubernetes.Clientset, logger logr.Logger) error { - var ( - podName = c.String("pod-name") - timeout = time.Duration(c.Int("timeout")) * time.Second - ) - if timeout == 0 { - timeout = time.Second * 300 - } - - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - logger.Info("check local nodes.conf") - nodes, err := getClusterNodes(c, ctx, client, false, logger) - if err != nil { - logger.Error(err, "parse nodes.conf with unknown error") - return err - } - if nodes == nil { - logger.Info("no nodes found") - return nil - } - - self := nodes.Self() - if !self.IsJoined() { - logger.Info("node not joined") - return nil - } - - authInfo, err := getRedisAuthInfo(c) - if err != nil { - logger.Error(err, "load redis operator user info failed") - return err - } - - if self.Role == redis.MasterRole { - randInt := rand.Intn(50) + 1 - duration := time.Duration(randInt) * time.Second - logger.Info(fmt.Sprintf("Wait for %s to escape failover conflict", duration)) - time.Sleep(duration) - - pods, _ := getPodsOfShard(ctx, c, client, logger) - for _, pod := range pods { - if pod.GetName() == podName { - continue - } - if pod.GetDeletionTimestamp() != nil { - continue - } - // ignore not ready pod - if !func() bool { - for _, cont := range pod.Status.ContainerStatuses { - if cont.Name == "redis" && cont.Ready { - return true - } - } - return false - }() { - continue - } - - logger.Info(fmt.Sprintf("check pod %s", pod.GetName())) - if err := func() error { - container := getContainerByName(pod.Spec.Containers, "redis") - ipFamilyPrefer := func() string { - for _, env := range container.Env { - if env.Name == "IP_FAMILY_PREFER" { - return env.Value - } - } - return "" - }() - - addr := net.JoinHostPort(pod.Status.PodIP, "6379") - for _, podIp := range pod.Status.PodIPs { - ip, _ := netip.ParseAddr(podIp.IP) - if ip.Is6() && ipFamilyPrefer == string(v1.IPv6Protocol) { - addr = net.JoinHostPort(podIp.IP, "6379") - break - } else if ip.Is4() && ipFamilyPrefer == string(v1.IPv4Protocol) { - addr = net.JoinHostPort(podIp.IP, "6379") - break - } - } - - logger.Info("check node", "pod", pod.GetName(), "addr", addr) - redisClient := redis.NewClient(addr, *authInfo) - defer redisClient.Close() - - nodes, err := redisClient.Nodes(ctx) - if err != nil { - logger.Error(err, "load cluster nodes failed") - return err - } - currentNode := nodes.Self() - if currentNode.MasterID == "" { - return fmt.Errorf("unjoined node") - } else if currentNode.MasterID == self.ID { - mastrNode := nodes.Get(self.ID) - action := NoFailoverAction - if mastrNode.IsFailed() { - action = ForceFailoverAction - } - if err := doRedisFailover(ctx, redisClient, action, logger); err != nil { - return err - } - } else { - err := fmt.Errorf("as if the shard got multi master") - logger.Error(err, "failover aborted, let operator to fix this") - return err - } - return nil - }(); err != nil { - continue - } - break - } - } - - // wait for some time for nodes to sync info - time.Sleep(time.Second * 10) - - addr := net.JoinHostPort("local.inject", "6379") - redisClient := redis.NewClient(addr, *authInfo) - defer redisClient.Close() - - logger.Info("shutdown node") - if _, err = redisClient.Do(ctx, "SHUTDOWN"); err != nil && !errors.Is(err, io.EOF) { - logger.Error(err, "graceful shutdown failed") - } - return nil -} - -func getContainerByName(containers []v1.Container, name string) *v1.Container { - for _, container := range containers { - if container.Name == name { - return &container - } - } - return nil -} diff --git a/cmd/redis-tools/pkg/commands/cluster/sync.go b/cmd/redis-tools/pkg/commands/cluster/sync.go deleted file mode 100644 index 45bcfb2..0000000 --- a/cmd/redis-tools/pkg/commands/cluster/sync.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cluster - -import ( - "context" - "os" - "path" - - "github.com/go-logr/logr" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -// Sync -func SyncToLocal(ctx context.Context, client *kubernetes.Clientset, namespace, name, - workspace, target string, logger logr.Logger) ([]byte, error) { - - targetFile := path.Join(workspace, target) - // check if node.conf exists - if data, err := os.ReadFile(targetFile); len(data) > 0 { - // ignore if this file exists and not empty - return data, nil - } else if os.IsPermission(err) { - logger.Error(err, "no permission") - return nil, err - } - - var cm *v1.ConfigMap - if err := RetryGet(func() (err error) { - cm, err = client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) - return - }, 20); errors.IsNotFound(err) { - logger.Info("no synced nodes.conf found") - return nil, nil - } else if err != nil { - logger.Error(err, "get configmap failed", "name", name) - return nil, nil - } - - val := cm.Data[target] - logger.Info("sync backup nodes.conf to local", "file", targetFile, "nodes.conf", val) - return []byte(val), os.WriteFile(targetFile, []byte(val), 0644) -} diff --git a/cmd/redis-tools/pkg/commands/helper/acl_test.go b/cmd/redis-tools/pkg/commands/helper/acl_test.go deleted file mode 100644 index 9272007..0000000 --- a/cmd/redis-tools/pkg/commands/helper/acl_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helper - -import ( - "reflect" - "testing" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/types/user" -) - -func Test_formatACLSetCommand(t *testing.T) { - type args struct { - user *user.User - } - tests := []struct { - name string - args args - wantArgs []string - }{ - { - name: "default", - args: args{ - user: &user.User{ - Name: "default", - Role: user.RoleDeveloper, - Rules: []*user.Rule{ - { - DisallowedCommands: []string{"flushall", "flushdb"}, - }, - }, - }, - }, - wantArgs: []string{"user", "default", "-flushall", "-flushdb", "+@all", "nopass", "on"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotArgs := formatACLSetCommand(tt.args.user); !reflect.DeepEqual(gotArgs, tt.wantArgs) { - t.Errorf("formatACLSetCommand() = %v, want %v", gotArgs, tt.wantArgs) - } - }) - } -} diff --git a/cmd/redis-tools/pkg/commands/runner/sync.go b/cmd/redis-tools/pkg/commands/runner/sync.go deleted file mode 100644 index 0828305..0000000 --- a/cmd/redis-tools/pkg/commands/runner/sync.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package runner - -import ( - "context" - "path" - "strings" - "time" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/kubernetes/client" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/sync" - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -func SyncFromLocalToConfigMap(c *cli.Context, ctx context.Context, logger logr.Logger) error { - var ( - namespace = c.String("namespace") - podName = c.String("pod-name") - workspace = c.String("workspace") - nodeConfigName = c.String("node-config-name") - configMapPrefix = c.String("prefix") - syncInterval = c.Int64("interval") - ) - - client, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - // sync to local - name := strings.Join([]string{strings.TrimSuffix(configMapPrefix, "-"), podName}, "-") - ownRefs, err := commands.NewOwnerReference(ctx, client, namespace, podName) - if err != nil { - return cli.Exit(err, 1) - } - // start sync process - return WatchAndSync(ctx, client, namespace, name, workspace, nodeConfigName, syncInterval, ownRefs, logger) -} - -func WatchAndSync(ctx context.Context, client *kubernetes.Clientset, namespace, name, workspace, target string, - syncInterval int64, ownerRefs []v1.OwnerReference, logger logr.Logger) error { - - ctrl, err := sync.NewController(client, sync.ControllerOptions{ - Namespace: namespace, - ConfigMapName: name, - OwnerReferences: ownerRefs, - SyncInterval: time.Duration(syncInterval) * time.Second, - Filters: []sync.Filter{&sync.RedisClusterFilter{}}, - }, logger) - if err != nil { - return err - } - fileWathcer, _ := sync.NewFileWatcher(ctrl.Handler, logger) - - if err := fileWathcer.Add(path.Join(workspace, target)); err != nil { - logger.Error(err, "watch file failed, error=%s") - return cli.Exit(err, 1) - } - - go func() { - _ = fileWathcer.Run(ctx) - }() - return ctrl.Run(ctx) -} diff --git a/cmd/redis-tools/pkg/commands/sync/command.go b/cmd/redis-tools/pkg/commands/sync/command.go deleted file mode 100644 index 11df193..0000000 --- a/cmd/redis-tools/pkg/commands/sync/command.go +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sync - -import ( - "context" - "crypto/tls" - "strings" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/kubernetes/client" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/logger" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" - "github.com/urfave/cli/v2" -) - -func NewCommand(ctx context.Context) *cli.Command { - return &cli.Command{ - Name: "sync", - Usage: "[Deprecated] Sync configfile from/to configmap", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "namespace", - Usage: "Namespace of current pod", - EnvVars: []string{"NAMESPACE"}, - }, - &cli.StringFlag{ - Name: "pod-name", - Usage: "The name of current pod", - EnvVars: []string{"POD_NAME"}, - }, - &cli.StringFlag{ - Name: "workspace", - Usage: "Workspace of this container", - Value: "/data", - }, - &cli.StringFlag{ - Name: "node-config-name", - Usage: "Node config file name", - Value: "nodes.conf", - }, - &cli.StringFlag{ - Name: "prefix", - Usage: "Configmap name prefix", - Value: "sync-", - }, - }, - Subcommands: []*cli.Command{ - { - Name: "c2l", - Usage: "Sync configfile from configmap to local", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "password", - Usage: "Redis instance password", - EnvVars: []string{"REDIS_PASSWORD"}, - }, - &cli.StringFlag{ - Name: "tls-key-file", - Usage: "Name of the client key file (including full path) if the server requires TLS client authentication", - EnvVars: []string{"TLS_CLIENT_KEY_FILE"}, - }, - &cli.StringFlag{ - Name: "tls-cert-file", - Usage: "Name of the client certificate file (including full path) if the server requires TLS client authentication", - EnvVars: []string{"TLS_CLIENT_CERT_FILE"}, - }, - }, - Action: func(c *cli.Context) error { - var ( - namespace = c.String("namespace") - podName = c.String("pod-name") - workspace = c.String("workspace") - nodeConfigName = c.String("node-config-name") - configMapPrefix = c.String("prefix") - password = c.String("password") - tlsKeyFile = c.String("tls-key-file") - tlsCertFile = c.String("tls-cert-file") - ) - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - logger := logger.NewLogger(c) - - client, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - - // sync to local - name := strings.Join([]string{strings.TrimSuffix(configMapPrefix, "-"), podName}, "-") - - opts := redis.AuthInfo{ - Password: password, - } - if tlsKeyFile != "" && tlsCertFile != "" { - cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) - if err != nil { - logger.Error(err, "load tls certificates failed") - return cli.Exit(err, 1) - } - opts.TLSConf = &tls.Config{ - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: true, - } - } - if err := SyncToLocal(ctx, client, namespace, name, workspace, nodeConfigName, opts, logger); err != nil { - return cli.Exit(err, 1) - } - return nil - }, - }, - { - Name: "l2c", - Usage: "Sync configfile from local to configmap", - Flags: []cli.Flag{ - &cli.Int64Flag{ - Name: "interval", - Usage: "Configmap sync interval", - Value: 5, - DefaultText: "5s", - }, - }, - Action: func(c *cli.Context) error { - var ( - namespace = c.String("namespace") - podName = c.String("pod-name") - workspace = c.String("workspace") - nodeConfigName = c.String("node-config-name") - configMapPrefix = c.String("prefix") - syncInterval = c.Int64("interval") - ) - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - logger := logger.NewLogger(c) - - client, err := client.NewClient() - if err != nil { - logger.Error(err, "create k8s client failed, error=%s", err) - return cli.Exit(err, 1) - } - - name := strings.Join([]string{strings.TrimSuffix(configMapPrefix, "-"), podName}, "-") - ownRefs, err := NewOwnerReference(ctx, client, namespace, podName) - if err != nil { - return cli.Exit(err, 1) - } - // start sync process - return WatchAndSync(ctx, client, namespace, name, workspace, nodeConfigName, syncInterval, ownRefs, logger) - }, - }, - }, - } -} diff --git a/cmd/redis-tools/pkg/commands/sync/sync.go b/cmd/redis-tools/pkg/commands/sync/sync.go deleted file mode 100644 index ce6576c..0000000 --- a/cmd/redis-tools/pkg/commands/sync/sync.go +++ /dev/null @@ -1,223 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sync - -import ( - "context" - "fmt" - "os" - "path" - "time" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/commands/cluster" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/sync" - "github.com/go-logr/logr" - "github.com/urfave/cli/v2" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -func getRedisNodeInfo(redisClient redis.Client, logger logr.Logger) (*redis.Node, []*redis.Node, error) { - nodes, err := redisClient.Nodes(context.TODO()) - if err != nil { - return nil, nil, err - } - return nodes.Self(), nodes, nil -} - -/* -func redisFailover(redisClient redis.Client, logger logr.Logger) error { - self, _, err := getRedisNodeInfo(redisClient, logger) - if err != nil { - return err - } - - logger.Info("try to do redis failover", "detail", self) - if self.Role == redis.MasterRole { - return nil - } - - if _, err := redisClient.Do(context.TODO(), "cluster", "failover", "force"); err != nil { - return err - } - - for i := 0; ; i++ { - time.Sleep(time.Second * 5) - - self, _, err := getRedisNodeInfo(redisClient, logger) - if err != nil || self == nil { - return err - } - if self.Role == redis.MasterRole { - return nil - } - if i > 6 { - logger.Error(fmt.Errorf("failover blocked"), "failover take too long time, please check cluster status manually and do manual failover. When fixed, restart the pod again", "addr", self.Addr) - } - } -} -*/ - -type FailoverAction string - -const ( - NoFailoverAction FailoverAction = "" - ForceFailoverAction FailoverAction = "FORCE" - TakeoverFailoverAction FailoverAction = "TAKEOVER" -) - -func doRedisFailover(ctx context.Context, cli redis.Client, action FailoverAction, logger logr.Logger) (err error) { - args := []interface{}{"FAILOVER"} - if action != "" { - args = append(args, action) - } - if _, err := cli.Do(ctx, "CLUSTER", args...); err != nil { - logger.Error(err, "do failover failed", "action", action) - return err - } - - logger.Info("wait 5s for failover") - time.Sleep(time.Second * 5) - - nodes, err := cli.Nodes(ctx) - if err != nil { - logger.Error(err, "fetch cluster nodes failed") - return err - } - self := nodes.Self() - if self == nil { - return fmt.Errorf("get nodes info failed, as if the nodes.conf is broken") - } - if self.Role == redis.MasterRole { - logger.Info("failover succeed") - return nil - } - if action == ForceFailoverAction { - return doRedisFailover(ctx, cli, TakeoverFailoverAction, logger) - } - return fmt.Errorf("do manual failover failed") -} - -// Sync -func SyncToLocal(ctx context.Context, client *kubernetes.Clientset, namespace, name, workspace, target string, redisOptions redis.AuthInfo, logger logr.Logger) error { - // check if node.conf exists - targetFile := path.Join(workspace, target) - nodesConfData := "" - if data, err := os.ReadFile(targetFile); err != nil && !os.IsNotExist(err) { - logger.Error(err, "load local config failed") - return err - } else if len(data) == 0 { - var cm *v1.ConfigMap - if err := cluster.RetryGet(func() (err error) { - cm, err = client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) - return - }, 3); errors.IsNotFound(err) { - return nil - } else if err != nil { - logger.Error(err, "get configmap failed", "name", name) - return err - } - nodesConfData = cm.Data[target] - } else { - if err := os.WriteFile(fmt.Sprintf("%s.bak.1", targetFile), data, 0644); err != nil { - logger.Error(err, "backup file failed", "file", targetFile) - } - nodesConfData = string(data) - } - - if len(nodesConfData) > 0 { - nodes := redis.ParseNodes(string(nodesConfData)) - self := nodes.Self() - if self == nil { - logger.Error(fmt.Errorf("get self node info failed"), "invalid node config file") - return nil - } - slaves := nodes.Replicas(self.ID) - - logger.Info("checking node", "id", self.ID, "role", self.Role, "slaves", slaves) - if self.Role == redis.MasterRole { - var ( - newMaster *redis.Node - firstSlave *redis.Node - ) - for _, slave := range slaves { - if end := func() bool { - redisClient := redis.NewClient(slave.Addr, redisOptions) - defer redisClient.Close() - - slaveNodeInfo, _, err := getRedisNodeInfo(redisClient, logger) - if err != nil { - logger.Error(err, "get slave info failed", "addr", slave.Addr) - return false - } - if slaveNodeInfo != nil && slaveNodeInfo.Role == redis.MasterRole { - newMaster = slaveNodeInfo - return true - } - if firstSlave == nil { - firstSlave = slaveNodeInfo - } - return false - }(); end { - break - } - } - - if newMaster == nil && firstSlave != nil { - redisClient := redis.NewClient(firstSlave.Addr, redisOptions) - defer redisClient.Close() - - if err := doRedisFailover(ctx, redisClient, ForceFailoverAction, logger); err != nil { - logger.Error(err, "redis failover failed", "addr", firstSlave.Addr) - } else { - logger.Info("failover succeed", "master", firstSlave.Addr) - } - } - } - _ = os.WriteFile(targetFile, []byte(nodesConfData), 0644) - } - return nil -} - -func WatchAndSync(ctx context.Context, client *kubernetes.Clientset, namespace, name, workspace, target string, - syncInterval int64, ownerRefs []metav1.OwnerReference, logger logr.Logger) error { - - ctrl, err := sync.NewController(client, sync.ControllerOptions{ - Namespace: namespace, - ConfigMapName: name, - OwnerReferences: ownerRefs, - SyncInterval: time.Duration(syncInterval) * time.Second, - Filters: []sync.Filter{&sync.RedisClusterFilter{}}, - }, logger) - if err != nil { - return err - } - fileWathcer, _ := sync.NewFileWatcher(ctrl.Handler, logger) - - if err := fileWathcer.Add(path.Join(workspace, target)); err != nil { - logger.Error(err, "watch file failed, error=%s") - return cli.Exit(err, 1) - } - - go func() { - _ = fileWathcer.Run(ctx) - }() - return ctrl.Run(ctx) -} diff --git a/cmd/redis-tools/pkg/commands/sync/util.go b/cmd/redis-tools/pkg/commands/sync/util.go deleted file mode 100644 index ba65207..0000000 --- a/cmd/redis-tools/pkg/commands/sync/util.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sync - -import ( - "context" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -// NewOwnerReference -func NewOwnerReference(ctx context.Context, client *kubernetes.Clientset, namespace, podName string) ([]metav1.OwnerReference, error) { - if client == nil { - return nil, nil - } - - pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) - if err != nil { - return nil, err - } - var name string - for _, ownerRef := range pod.OwnerReferences { - if ownerRef.Kind == "StatefulSet" { - name = ownerRef.Name - break - } - } - if sts, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}); err != nil { - return nil, err - } else { - return sts.OwnerReferences, nil - } -} diff --git a/cmd/redis-tools/pkg/commands/util.go b/cmd/redis-tools/pkg/commands/util.go deleted file mode 100644 index 4be2097..0000000 --- a/cmd/redis-tools/pkg/commands/util.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package commands - -import ( - "context" - "crypto/tls" - "fmt" - "os" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/redis" - "github.com/urfave/cli/v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -const ( - DefaultSecretMountPath = "/account/password" -) - -func LoadProxyAuthInfo(c *cli.Context, ctx context.Context) (*redis.AuthInfo, error) { - - if data, err := os.ReadFile(DefaultSecretMountPath); err != nil { - return &redis.AuthInfo{}, err - } else { - return &redis.AuthInfo{ - Password: string(data), - }, nil - } -} - -func LoadAuthInfo(c *cli.Context, ctx context.Context) (*redis.AuthInfo, error) { - var ( - // acl - opUsername = c.String("operator-username") - opSecret = c.String("operator-secret-name") - - // tls - isTLSEnabled = c.Bool("tls") - tlsKeyFile = c.String("tls-key-file") - tlsCertFile = c.String("tls-cert-file") - ) - - var ( - err error - tlsConf *tls.Config - password string - ) - - if opSecret != "" { - if data, err := os.ReadFile(DefaultSecretMountPath); err != nil { - return nil, err - } else { - password = string(data) - } - } - - if isTLSEnabled { - if tlsConf, err = LoadTLSCofig(tlsKeyFile, tlsCertFile); err != nil { - return nil, err - } - } - return &redis.AuthInfo{ - Username: opUsername, - Password: password, - TLSConf: tlsConf, - }, nil -} - -func LoadTLSCofig(tlsKeyFile, tlsCertFile string) (*tls.Config, error) { - if tlsKeyFile == "" || tlsCertFile == "" { - return nil, fmt.Errorf("tls file path not configed") - } - cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) - if err != nil { - return nil, err - } - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: true, - }, nil -} - -// NewOwnerReference -func NewOwnerReference(ctx context.Context, client *kubernetes.Clientset, namespace, podName string) ([]metav1.OwnerReference, error) { - if client == nil { - return nil, nil - } - - pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) - if err != nil { - return nil, err - } - var name string - for _, ownerRef := range pod.OwnerReferences { - if ownerRef.Kind == "StatefulSet" { - name = ownerRef.Name - break - } - } - if sts, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}); err != nil { - return nil, err - } else { - return sts.OwnerReferences, nil - } -} diff --git a/cmd/redis-tools/pkg/kubernetes/client/client.go b/cmd/redis-tools/pkg/kubernetes/client/client.go deleted file mode 100644 index aaee3f3..0000000 --- a/cmd/redis-tools/pkg/kubernetes/client/client.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package client - -import ( - "fmt" - "os" - "path/filepath" - - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" -) - -func NewClient() (*kubernetes.Clientset, error) { - var ( - err error - conf *rest.Config - ) - - host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") - if host == "" && port == "" { - if fp := os.Getenv("KUBE_CONFIG"); fp != "" { - if conf, err = clientcmd.BuildConfigFromFlags("", fp); err != nil { - return nil, fmt.Errorf("load config from $KUBE_CONFIG failed, error=%s", err) - } - } else { - if home := homedir.HomeDir(); home != "" { - fp := filepath.Join(home, ".kube", "config") - if conf, err = clientcmd.BuildConfigFromFlags("", fp); err != nil { - return nil, fmt.Errorf("load config from local .kube/config failed, error=%s", err) - } - } else { - return nil, fmt.Errorf("no local config found") - } - } - } else { - conf, err = rest.InClusterConfig() - if err != nil { - return nil, err - } - } - return kubernetes.NewForConfig(conf) -} - -func NewDynamicClient() (dynamic.Interface, error) { - var ( - err error - conf *rest.Config - ) - - host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") - if host == "" && port == "" { - if fp := os.Getenv("KUBE_CONFIG"); fp != "" { - if conf, err = clientcmd.BuildConfigFromFlags("", fp); err != nil { - return nil, fmt.Errorf("load config from $KUBE_CONFIG failed, error=%s", err) - } - } else { - if home := homedir.HomeDir(); home != "" { - fp := filepath.Join(home, ".kube", "config") - if conf, err = clientcmd.BuildConfigFromFlags("", fp); err != nil { - return nil, fmt.Errorf("load config from local .kube/config failed, error=%s", err) - } - } else { - return nil, fmt.Errorf("no local config found") - } - } - } else { - conf, err = rest.InClusterConfig() - if err != nil { - return nil, err - } - } - return dynamic.NewForConfig(conf) -} diff --git a/cmd/redis-tools/pkg/redis/client.go b/cmd/redis-tools/pkg/redis/client.go deleted file mode 100644 index 40d547d..0000000 --- a/cmd/redis-tools/pkg/redis/client.go +++ /dev/null @@ -1,227 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redis - -import ( - "context" - "crypto/tls" - "strings" - "time" - - "github.com/gomodule/redigo/redis" -) - -// alias -var ( - ErrNil = redis.ErrNil - ErrPoolExhausted = redis.ErrPoolExhausted -) - -var ( - Bool = redis.Bool - ByteSlices = redis.ByteSlices - Bytes = redis.Bytes - Float64 = redis.Float64 - Float64Map = redis.Float64Map - Float64s = redis.Float64s - Int = redis.Int - IntMap = redis.IntMap - Ints = redis.Ints - Int64 = redis.Int64 - Int64Map = redis.Int64Map - Int64s = redis.Int64s - Uint64 = redis.Uint64 - Uint64Map = redis.Uint64Map - Uint64s = redis.Uint64s - Values = redis.Values - Positions = redis.Positions - Scan = redis.Scan - ScanSlice = redis.ScanSlice - ScanStruct = redis.ScanStruct - String = redis.String - Strings = redis.Strings -) - -type Client interface { - Nodes(ctx context.Context) (Nodes, error) - Do(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) - Close() error - Clone(ctx context.Context, addr string) Client - CheckProxyInfo(ctx context.Context) error -} - -type _RedisClient struct { - pool *redis.Pool - authInfo *AuthInfo -} - -type AuthInfo struct { - Username string - Password string - TLSConf *tls.Config -} - -func ipv6ToURL(ipv6 string) string { - - // Split the address into IP and port parts - parts := strings.Split(ipv6, ":") - - // Join the IP parts together with colons - ip := strings.Join(parts[:len(parts)-1], ":") - - // Surround the IP address with brackets - ip = "[" + ip + "]" - - // Combine the IP and port back together - address := ip + ":" + parts[len(parts)-1] - - return address -} - -// For the purpose of special handling the information returned by cluster nodes -func getAddress(address string) string { - if strings.Contains(address, "]") { - return address - } - if strings.Count(address, ":") > 1 { - return ipv6ToURL(address) - } - return address - -} - -// NewClient -func NewClient(addr string, authInfo AuthInfo) Client { - client := _RedisClient{authInfo: &authInfo} - addr_formated := getAddress(addr) - client.pool = &redis.Pool{ - DialContext: func(ctx context.Context) (redis.Conn, error) { - var opts []redis.DialOption - if authInfo.Password != "" { - if authInfo.Username != "" && authInfo.Username != "default" { - opts = append(opts, redis.DialUsername(authInfo.Username)) - } - opts = append(opts, redis.DialPassword(authInfo.Password)) - } - if authInfo.TLSConf != nil { - opts = append(opts, - redis.DialUseTLS(true), - redis.DialTLSConfig(authInfo.TLSConf), - redis.DialTLSSkipVerify(true), - ) - } - - ctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - - conn, err := redis.DialContext(ctx, "tcp", addr_formated, opts...) - if err != nil { - return nil, err - } - return conn, nil - }, - MaxIdle: 1, - MaxActive: 1, - TestOnBorrow: func(c redis.Conn, t time.Time) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - if _, err := redis.DoContext(c, ctx, "PING"); err != nil { - return err - } - return nil - }, - } - return &client -} - -// Do -func (c *_RedisClient) Do(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) { - if c == nil { - return nil, nil - } - - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - conn, err := c.pool.GetContext(ctx) - if err != nil { - return nil, err - } - defer conn.Close() - - return redis.DoContext(conn, ctx, cmd, args...) -} - -func (c *_RedisClient) Close() error { - if c == nil || c.pool == nil { - return nil - } - return c.pool.Close() -} - -func (c *_RedisClient) Clone(ctx context.Context, addr string) Client { - if c == nil || c.authInfo == nil { - return nil - } - - return NewClient(addr, *c.authInfo) -} - -// Nodes -func (c *_RedisClient) Nodes(ctx context.Context) (Nodes, error) { - if c == nil { - return nil, nil - } - - data, err := String(c.Do(ctx, "cluster", "nodes")) - if err != nil { - return nil, err - } - return ParseNodes(data), nil -} - -func (c *_RedisClient) CheckProxyInfo(ctx context.Context) error { - if c == nil { - return nil - } - - data, err := String(c.Do(ctx, "info")) - if err != nil { - return err - } - proxyinfo := ParseProxyInfo(data) - if proxyinfo.FailCount >= 3 || proxyinfo.UnknownRole >= 3 { - err := c.tryTestGet(ctx) - if err != nil { - return err - } - } - return err -} - -// 尝试get __ALAUDA_REDIS_PROXY_TEST_KEY__ -func (c *_RedisClient) tryTestGet(ctx context.Context) error { - if c == nil { - return nil - } - _, err := String(c.Do(ctx, "get", "__ALAUDA_REDIS_PROXY_TEST_KEY__")) - if err != nil { - return err - } - return nil -} diff --git a/cmd/redis-tools/pkg/redis/client_test.go b/cmd/redis-tools/pkg/redis/client_test.go deleted file mode 100644 index b793a64..0000000 --- a/cmd/redis-tools/pkg/redis/client_test.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redis - -import "testing" - -func TestIPv6ToURL(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "IPv6 with port", - input: "2001:0db8:85a3:0000:0000:8a2e:0370:7334:8080", - expected: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080", - }, - { - name: "IPv4 address", - input: "192.168.1.1:8080", - expected: "192.168.1.1:8080", - }, - { - name: "IPv6 address with bracket", - input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080", - expected: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - output := getAddress(test.input) - if output != test.expected { - t.Errorf("Expected %s, but got %s", test.expected, output) - } - }) - } -} diff --git a/cmd/redis-tools/pkg/redis/node.go b/cmd/redis-tools/pkg/redis/node.go deleted file mode 100644 index f03a747..0000000 --- a/cmd/redis-tools/pkg/redis/node.go +++ /dev/null @@ -1,207 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redis - -import ( - "encoding/json" - "regexp" - "strconv" - "strings" - - "github.com/alauda/redis-operator/cmd/redis-tools/pkg/types/slot" -) - -const ( - MasterRole = "master" - SlaveRole = "slave" -) - -type Node struct { - ID string - Addr string - Flags string - Role string - MasterID string - PingSend int64 - PingRecv int64 - ConfigEpoch int64 - LinkState string - slots []string - rawInfo string - // slaves []*Node -} - -func (n *Node) IsSelf() bool { - if n == nil { - return false - } - return strings.HasPrefix(n.Flags, "myself") -} - -func (n *Node) IsFailed() bool { - if n == nil { - return true - } - return strings.Contains(n.Flags, "fail") -} - -func (n *Node) IsConnected() bool { - if n == nil { - return false - } - return n.LinkState == "connected" -} - -func (n *Node) IsJoined() bool { - if n == nil { - return false - } - return n.Addr != "" -} - -func (n *Node) Slots() *slot.Slots { - if n == nil { - return nil - } - if n == nil || n.Role == SlaveRole || len(n.slots) == 0 { - return nil - } - - slots := slot.NewSlots() - _ = slots.Load(n.slots) - - return slots -} - -func (n *Node) Raw() string { - if n == nil { - return "" - } - return n.rawInfo -} - -type Nodes []*Node - -func (ns Nodes) Get(id string) *Node { - for _, n := range ns { - if n.ID == id { - return n - } - } - return nil -} - -func (ns Nodes) Self() *Node { - for _, n := range ns { - if n.IsSelf() { - return n - } - } - return nil -} - -func (ns Nodes) Replicas(id string) (ret []*Node) { - for _, n := range ns { - if n.MasterID == id { - ret = append(ret, n) - } - } - return -} - -func (ns Nodes) Masters() (ret []*Node) { - for _, n := range ns { - if n.Role == MasterRole && len(n.slots) > 0 { - ret = append(ret, n) - } - } - return -} - -func (ns Nodes) Marshal() ([]byte, error) { - data := []map[string]string{} - for _, n := range ns { - d := map[string]string{} - d["id"] = n.ID - d["addr"] = n.Addr - d["flags"] = n.Flags - d["role"] = n.Role - d["master_id"] = n.MasterID - d["ping_send"] = strconv.FormatInt(n.PingSend, 10) - d["ping_recv"] = strconv.FormatInt(n.PingRecv, 10) - d["config_epoch"] = strconv.FormatInt(n.ConfigEpoch, 10) - d["link_state"] = n.LinkState - d["slots"] = strings.Join(n.slots, ",") - data = append(data, d) - } - return json.Marshal(data) -} - -var ( - invalidAddrReg = regexp.MustCompile(`^:\d+$`) -) - -// parseNodes -// -// format: -// -// ... -func ParseNodes(data string) (nodes Nodes) { - lines := strings.Split(data, "\n") - for _, line := range lines { - if strings.HasPrefix(line, "vars") { - continue - } - fields := strings.Fields(line) - if len(fields) < 8 { - continue - } - addrPair := strings.SplitN(fields[1], "@", 2) - if len(addrPair) != 2 { - continue - } - addr := addrPair[0] - if invalidAddrReg.MatchString(addr) { - addr = "" - } - role := fields[2] - if strings.Contains(fields[2], SlaveRole) { - role = SlaveRole - } else if strings.Contains(fields[2], MasterRole) { - role = MasterRole - } - pingSend, _ := strconv.ParseInt(fields[4], 10, 64) - pongRecv, _ := strconv.ParseInt(fields[5], 10, 64) - epoch, _ := strconv.ParseInt(fields[6], 10, 64) - - node := &Node{ - ID: fields[0], - Addr: addr, - Flags: fields[2], - Role: role, - MasterID: strings.TrimPrefix(fields[3], "-"), - PingSend: pingSend, - PingRecv: pongRecv, - ConfigEpoch: epoch, - LinkState: fields[7], - slots: fields[8:], - rawInfo: line, - } - nodes = append(nodes, node) - } - return -} diff --git a/cmd/redis-tools/pkg/redis/proxy.go b/cmd/redis-tools/pkg/redis/proxy.go deleted file mode 100644 index b2be33d..0000000 --- a/cmd/redis-tools/pkg/redis/proxy.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redis - -import "strings" - -type ProxyInfo struct { - UnknownRole int - FailCount int -} - -func ParseProxyInfo(data string) ProxyInfo { - // data to line - proxyInfo := ProxyInfo{} - lines := strings.Split(data, "\n") - for _, line := range lines { - if line == "" || strings.HasPrefix(line, "#") { - continue - } - sl := strings.SplitN(line, ":", 2) - if len(sl) != 2 { - continue - } - if sl[0] == "Role" && sl[1] == "unknown" { - proxyInfo.UnknownRole++ - } - if sl[0] == "CurrentIsFail" && sl[1] == "1" { - proxyInfo.FailCount++ - } - } - return proxyInfo -} diff --git a/cmd/redis-tools/pkg/sync/controller.go b/cmd/redis-tools/pkg/sync/controller.go deleted file mode 100644 index 20f521d..0000000 --- a/cmd/redis-tools/pkg/sync/controller.go +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sync - -import ( - "context" - rerrors "errors" - "fmt" - "os" - "path" - "time" - - "github.com/go-logr/logr" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -type UpdateEvent struct { - FilePath string - Data string - Timestamp int64 -} - -type Filter interface { - Truncate(filename string, data string) (string, error) -} - -type ControllerOptions struct { - Namespace string - ConfigMapName string - OwnerReferences []metav1.OwnerReference - SyncInterval time.Duration - Filters []Filter -} - -// Controller -type Controller struct { - cmManager *ConfigManager - - dataUpdateChan chan *UpdateEvent - filters []Filter - - options ControllerOptions - logger logr.Logger -} - -func NewController(client *kubernetes.Clientset, options ControllerOptions, logger logr.Logger) (*Controller, error) { - logger = logger.WithName("Controller") - - if options.SyncInterval <= time.Second*3 { - options.SyncInterval = time.Second * 10 - } - if options.Namespace == "" { - return nil, fmt.Errorf("namespace required") - } - if options.ConfigMapName == "" { - return nil, fmt.Errorf("configmap name required") - } - if len(options.OwnerReferences) == 0 { - logger.Info("WARNING: resource owner reference not specified") - } - - cmManager, err := NewConfigManager(client, logger) - if err != nil { - logger.Error(err, "create ConfigManager failed") - return nil, err - } - - ctrl := Controller{ - cmManager: cmManager, - dataUpdateChan: make(chan *UpdateEvent, 100), - options: options, - filters: options.Filters, - logger: logger, - } - return &ctrl, nil -} - -func (c *Controller) Handler(ctx context.Context, fs *FileStat) error { - logger := c.logger.WithName("Handler") - - data, err := os.ReadFile(fs.FilePath()) - if err != nil { - logger.Error(err, "load file content failed", "filepath", fs.FilePath()) - return err - } - - select { - case <-ctx.Done(): - return ctx.Err() - case c.dataUpdateChan <- &UpdateEvent{FilePath: fs.FilePath(), Data: string(data)}: - } - return nil -} - -func (c *Controller) Run(ctx context.Context) error { - logger := c.logger - - getConfigMap := func() (*v1.ConfigMap, error) { - configMap, err := c.cmManager.Get(ctx, c.options.Namespace, c.options.ConfigMapName) - if errors.IsNotFound(err) { - configMap = &v1.ConfigMap{} - configMap.Namespace = c.options.Namespace - configMap.Name = c.options.ConfigMapName - configMap.OwnerReferences = c.options.OwnerReferences - - if err := c.cmManager.Save(ctx, configMap); err != nil { - logger.Error(err, "init configmap failed", "namespace", configMap.Namespace, "name", configMap.Name) - } - } else if rerrors.Is(err, context.Canceled) { - return nil, nil - } else if err != nil { - logger.Error(err, "get configmap failed", "namespace", configMap.Namespace, "name", configMap.Name) - return nil, err - } - - if configMap.Data == nil { - configMap.Data = map[string]string{} - } - return configMap, nil - } - - configMap, err := getConfigMap() - if err != nil { - return err - } - - ticker := time.NewTicker(c.options.SyncInterval) - defer ticker.Stop() - - isChanged := int64(0) - for { - select { - case <-ctx.Done(): - return nil - case event, ok := <-c.dataUpdateChan: - if !ok { - return nil - } - - oldSize := configMap.Size() - oldData := configMap.Data[event.FilePath] - - // etcd limiteds configmap/secret size limit to 1Mi - if oldSize-len(oldData)+len(event.Data) >= 1024*1024*1024-4096 { - logger.Error(fmt.Errorf("data size has exceed 1Mi"), "filepath", event.FilePath) - continue - } - - if event.Data == oldData { - continue - } - - filename := path.Base(event.FilePath) - if func() bool { - for _, filter := range c.filters { - if data, err := filter.Truncate(filename, event.Data); err != nil { - return false - } else { - event.Data = data - } - } - return true - }() { - configMap.Data[filename] = event.Data - isChanged += 1 - } - case <-ticker.C: - if isChanged == 0 { - continue - } - - func() { - ctx, cancel := context.WithTimeout(ctx, c.options.SyncInterval-time.Second) - defer cancel() - - if err := c.cmManager.Save(ctx, configMap); err != nil { - logger.Error(err, "update configmap failed") - } - }() - isChanged = 0 - } - } -} diff --git a/cmd/redis-tools/pkg/sync/manager.go b/cmd/redis-tools/pkg/sync/manager.go deleted file mode 100644 index 09f4ca8..0000000 --- a/cmd/redis-tools/pkg/sync/manager.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sync - -import ( - "context" - "fmt" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -type ConfigManager struct { - client *kubernetes.Clientset - logger logr.Logger -} - -func NewConfigManager(client *kubernetes.Clientset, logger logr.Logger) (*ConfigManager, error) { - if client == nil { - return nil, fmt.Errorf("Clientset required") - } - - cm := ConfigManager{ - client: client, - logger: logger.WithName("ConfigManager"), - } - return &cm, nil -} - -// Get -func (m *ConfigManager) Get(ctx context.Context, namespace, name string) (*corev1.ConfigMap, error) { - cm, err := m.client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - return cm, nil -} - -// Save -func (m *ConfigManager) Save(ctx context.Context, cm *corev1.ConfigMap) error { - if cm == nil { - return nil - } - - oldCm, err := m.client.CoreV1().ConfigMaps(cm.Namespace).Get(ctx, cm.Name, metav1.GetOptions{}) - if errors.IsNotFound(err) { - if _, err := m.client.CoreV1().ConfigMaps(cm.Namespace).Create(ctx, cm, metav1.CreateOptions{}); err != nil { - m.logger.Error(err, "create configmap failed", "namespace", cm.Namespace, "name", cm.Name) - return err - } - return nil - } else if err != nil { - m.logger.Error(err, "check configmap status failed", "namespace", cm.Namespace, "name", cm.Name) - return err - } - oldCm.Data = cm.Data - if _, err := m.client.CoreV1().ConfigMaps(cm.Namespace).Update(ctx, oldCm, metav1.UpdateOptions{}); err != nil { - m.logger.Error(err, "update configmap failed", "namespace", cm.Namespace, cm.Name) - return err - } - return nil -} - -// Delete -func (m *ConfigManager) Delete(ctx context.Context, namespace, name string) error { - err := m.client.CoreV1().ConfigMaps(namespace).Delete(ctx, name, metav1.DeleteOptions{}) - if err != nil { - m.logger.Error(err, "delete configmap failed", "namespace", namespace, name) - return err - } - return nil -} diff --git a/cmd/redis-tools/pkg/sync/redis.go b/cmd/redis-tools/pkg/sync/redis.go deleted file mode 100644 index d4eef29..0000000 --- a/cmd/redis-tools/pkg/sync/redis.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sync - -import ( - "errors" -) - -type RedisClusterFilter struct{} - -func (v *RedisClusterFilter) Truncate(filename string, data string) (string, error) { - if len(data) == 0 { - return "", errors.New("empty data") - } - return data, nil -} diff --git a/cmd/redis-tools/pkg/types/slot/slot.go b/cmd/redis-tools/pkg/types/slot/slot.go deleted file mode 100644 index 8fcdc9e..0000000 --- a/cmd/redis-tools/pkg/types/slot/slot.go +++ /dev/null @@ -1,431 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package slot - -import ( - "errors" - "fmt" - "strconv" - "strings" -) - -const ( - RedisMaxSlots = 16384 -) - -// SlotAssignStatus slot assign status -type SlotAssignStatus int - -const ( - // SlotUnAssigned - slot assigned - SlotUnAssigned SlotAssignStatus = 0 - // SlotImporting - slot is in importing status - SlotImporting SlotAssignStatus = 1 - // SlotAssigned - slot is assigned - SlotAssigned SlotAssignStatus = 2 - // SlotMigrating - slot is in migrating status - SlotMigrating SlotAssignStatus = 3 -) - -func NewSlotAssignStatusFromString(v string) SlotAssignStatus { - switch v { - case "SlotImporting": - return 1 - case "SlotAssigned": - return 2 - case "SlotMigrating": - return 3 - } - return 0 -} - -func (s SlotAssignStatus) String() string { - switch s { - case SlotImporting: - return "SlotImporting" - case SlotMigrating: - return "SlotMigrating" - case SlotAssigned: - return "SlotAssigned" - } - return "SlotUnAssigned" -} - -// Slots use two byte represent the slot status -// 00 - unassigned -// 01 - importing -// 10 - assigned -// 11 - migrating -// Slots - -type Slots struct { - data [4096]uint8 - migratingSlots map[int]string - importingSlots map[int]string -} - -func NewSlots() *Slots { - return &Slots{ - migratingSlots: map[int]string{}, - importingSlots: map[int]string{}, - } -} - -// IsFullfilled check if this slots if fullfilled -func (s *Slots) IsFullfilled() bool { - if s == nil { - return false - } - - for i := 0; i < RedisMaxSlots; i++ { - index, offset := s.convertIndex(i) - - if (s.data[index]>>offset)&uint8(SlotAssigned) == 0 { - return false - } - } - return true -} - -func (s *Slots) parseStrSlots(v string) (ret map[int]SlotAssignStatus, nodes map[int]string, err error) { - ret = map[int]SlotAssignStatus{} - nodes = map[int]string{} - - fields := strings.Fields(strings.ReplaceAll(v, ",", " ")) - for _, field := range fields { - field = strings.TrimSuffix(strings.TrimPrefix(field, "["), "]") - if strings.Contains(field, "-<-") { - moveFields := strings.SplitN(field, "-<-", 2) - if len(moveFields) != 2 { - return nil, nil, fmt.Errorf("invalid slot %s", field) - } - start, err := strconv.ParseInt(moveFields[0], 10, 32) - if err != nil { - return nil, nil, fmt.Errorf("invalid slot %s", field) - } - ret[int(start)] = SlotImporting - nodes[int(start)] = moveFields[1] - } else if strings.Contains(field, "->-") { - moveFields := strings.SplitN(field, "->-", 2) - if len(moveFields) != 2 { - return nil, nil, fmt.Errorf("invalid slot %s", field) - } - start, err := strconv.ParseInt(moveFields[0], 10, 32) - if err != nil { - return nil, nil, fmt.Errorf("invalid slot %s", field) - } - ret[int(start)] = SlotMigrating - nodes[int(start)] = moveFields[1] - } else if strings.Contains(field, "-") { - rangeFields := strings.SplitN(field, "-", 2) - if len(rangeFields) != 2 { - return nil, nil, fmt.Errorf("invalid range slot %s", field) - } - start, err := strconv.ParseInt(rangeFields[0], 10, 32) - if err != nil { - return nil, nil, fmt.Errorf("invalid range slot %s", field) - } - end, err := strconv.ParseInt(rangeFields[1], 10, 32) - if err != nil { - return nil, nil, fmt.Errorf("invalid range slot %s", field) - } - if start > end { - return nil, nil, fmt.Errorf("invalid range slot %s", field) - } - for i := int(start); i <= int(end); i++ { - ret[i] = SlotAssigned - } - } else { - slotIndex, err := strconv.ParseInt(field, 10, 32) - if err != nil { - return nil, nil, fmt.Errorf("invalid range slot %s", field) - } - ret[int(slotIndex)] = SlotAssigned - } - } - return -} - -func (s *Slots) Load(v interface{}) error { - if s == nil { - return nil - } - handler := func(i int, status SlotAssignStatus, nodeId string) { - index, offset := s.convertIndex(i) - s.data[index] = s.data[index]&^(uint8(3)<> offset) & uint8(SlotAssigned)) - valB := ((n.data[index] >> offset) & uint8(SlotAssigned)) - if valA == uint8(SlotAssigned) && valB == 0 { - ret.data[index] |= (uint8(SlotAssigned) << offset) - } - } - return ret -} - -// Union ignore slot status -func (s *Slots) Union(slots ...*Slots) *Slots { - ret := NewSlots() - if s != nil { - ret.data = s.data - } - for i := 0; i < len(ret.data); i++ { - // clean slot status - ret.data[i] &= 0xAA - for j := 0; j < len(slots); j++ { - if slots[j] != nil { - ret.data[i] |= (slots[j].data[i] & 0xAA) - } - } - } - return ret -} - -// Slots -func (s *Slots) Slots(st SlotAssignStatus) (ret []int) { - if s == nil { - return nil - } - for i := 0; i < RedisMaxSlots; i++ { - index, offset := s.convertIndex(i) - if (s.data[index]>>offset)&uint8(SlotMigrating) == uint8(st) { - ret = append(ret, i) - } - } - return -} - -// String -func (s *Slots) String() string { - if s == nil { - return "" - } - - var ( - lastStartIndex = -1 - lastIndex = -1 - ret []string - ) - for i := 0; i < RedisMaxSlots; i++ { - index, offset := s.convertIndex(i) - if (s.data[index]>>offset)&uint8(SlotAssigned) > 0 { - if lastIndex == -1 { - lastIndex = i - lastStartIndex = i - } else if lastIndex != i-1 { - if lastStartIndex == lastIndex { - ret = append(ret, fmt.Sprintf("%d", lastStartIndex)) - } else { - ret = append(ret, fmt.Sprintf("%d-%d", lastStartIndex, lastIndex)) - } - lastIndex = i - lastStartIndex = i - } else { - lastIndex = i - } - } - } - if lastIndex != -1 { - if lastStartIndex == lastIndex { - ret = append(ret, fmt.Sprintf("%d", lastStartIndex)) - } else { - ret = append(ret, fmt.Sprintf("%d-%d", lastStartIndex, lastIndex)) - } - } - return strings.Join(ret, ",") -} - -func (s *Slots) Count(status SlotAssignStatus) (c int) { - if s == nil { - return - } - - mask := uint8(SlotMigrating) - if status == SlotUnAssigned || status == SlotAssigned { - mask = uint8(SlotAssigned) - } - - for i := 0; i < RedisMaxSlots; i++ { - index, offset := s.convertIndex(i) - if (s.data[index]>>offset)&mask == uint8(status) { - c += 1 - } - } - return -} - -func (s *Slots) Status(i int) SlotAssignStatus { - index, offset := s.convertIndex(i) - return SlotAssignStatus((s.data[index] >> offset) & 3) -} - -func (s *Slots) IsSet(i int) bool { - index, offset := s.convertIndex(i) - return (s.data[index]>>offset)&uint8(SlotAssigned) == uint8(SlotAssigned) -} - -func (s *Slots) IsImporting() bool { - if s == nil { - return false - } - return len(s.importingSlots) > 0 -} - -func (s *Slots) IsMigration() bool { - if s == nil { - return false - } - return len(s.migratingSlots) > 0 -} - -func (s *Slots) MoveingStatus(i int) (SlotAssignStatus, string) { - index, offset := s.convertIndex(i) - status := SlotAssignStatus((s.data[index] >> offset) & 3) - switch status { - case SlotImporting: - return status, s.importingSlots[i] - case SlotMigrating: - return status, s.migratingSlots[i] - } - return status, "" -} - -func (s *Slots) convertIndex(i int) (int, int) { - index := i * 2 / 8 - offset := i*2 - index*8 - return index, offset -} diff --git a/cmd/redis-tools/pkg/types/slot/slot_test.go b/cmd/redis-tools/pkg/types/slot/slot_test.go deleted file mode 100644 index 9a94b9d..0000000 --- a/cmd/redis-tools/pkg/types/slot/slot_test.go +++ /dev/null @@ -1,438 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package slot - -import ( - "reflect" - "testing" -) - -var ( - slotsA = &Slots{} - slotsB = &Slots{} - slotsC = &Slots{} - slotsD = &Slots{} - - slotsInterDB = &Slots{} - slotsInterDC = &Slots{} - allSlots *Slots -) - -func init() { - if err := slotsA.Set("0-5461", SlotAssigned); err != nil { - panic(err) - } - if err := slotsB.Set("5462-10922", SlotAssigned); err != nil { - panic(err) - } - if err := slotsC.Set("10923-16383", SlotAssigned); err != nil { - panic(err) - } - if err := slotsD.Set("0-5461,5464,10922,16000-16111", SlotAssigned); err != nil { - panic(err) - } - if err := slotsInterDB.Set("5464,10922", SlotAssigned); err != nil { - panic(err) - } - if err := slotsInterDC.Set("16000-16111", SlotAssigned); err != nil { - panic(err) - } - - allSlots = slotsA.Union(slotsB, slotsC) -} - -func TestSlots_IsFullfilled(t *testing.T) { - tests := []struct { - name string - s *Slots - wantFullfill bool - }{ - { - name: "slotsA", - s: slotsA, - wantFullfill: false, - }, - { - name: "slotsB", - s: slotsB, - wantFullfill: false, - }, - { - name: "slotsC", - s: slotsC, - wantFullfill: false, - }, - { - name: "allSlots", - s: allSlots, - wantFullfill: true, - }, - { - name: "slotsD", - s: slotsD, - wantFullfill: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.IsFullfilled(); got != tt.wantFullfill { - t.Errorf("Slots.IsFullfilled() = %v, want %v, %v", got, tt.wantFullfill, tt.s.String()) - } - }) - } -} - -func TestSlots_Inter(t *testing.T) { - type args struct { - n *Slots - } - tests := []struct { - name string - s *Slots - args args - want *Slots - }{ - { - name: "with_inter_d_b", - s: slotsD, - args: args{ - n: slotsB, - }, - want: slotsInterDB, - }, - { - name: "with_inter_d_c", - s: slotsD, - args: args{ - n: slotsC, - }, - want: slotsInterDC, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Inter(tt.args.n); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Slots.Inter() = %v, want %v", got.String(), tt.want.String()) - } - }) - } -} - -func TestSlots_Equals(t *testing.T) { - slotsA := Slots{} - slotsB := Slots{} - for i := 0; i < 1000; i++ { - if i < 500 { - status := SlotAssigned - if i%2 == 0 { - status = SlotMigrating - } - _ = slotsA.Set(i, SlotAssignStatus(status)) - } else { - _ = slotsA.Set(i, SlotImporting) - } - } - _ = slotsB.Set("0-499", SlotAssigned) - type args struct { - old *Slots - } - tests := []struct { - name string - s *Slots - args args - want bool - }{ - { - name: "equals", - s: &slotsA, - args: args{old: &slotsB}, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Equals(tt.args.old); got != tt.want { - t.Errorf("Slots.Equals() = %v, want %v %v %v", got, tt.want, tt.s.String(), tt.args.old.String()) - } - }) - } -} - -func TestSlots_Union(t *testing.T) { - type args struct { - slots []*Slots - } - tests := []struct { - name string - s *Slots - args args - want *Slots - }{ - { - name: "all", - s: slotsA, - args: args{slots: []*Slots{slotsB, slotsC}}, - want: allSlots, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Union(tt.args.slots...); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Slots.Union() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestSlots_String(t *testing.T) { - slots := Slots{} - _ = slots.Set(0, SlotAssigned) - _ = slots.Set("1-100", SlotImporting) - _ = slots.Set("1000-2000", SlotMigrating) - _ = slots.Set("5000-10000", SlotAssigned) - _ = slots.Set("16111,16121,16131", SlotImporting) - _ = slots.Set("16112,16122,16132", SlotMigrating) - _ = slots.Set("16113,16123,16153", SlotAssigned) - _ = slots.Set("5201,5233,5400", SlotUnAssigned) - - tests := []struct { - name string - s *Slots - wantRet string - }{ - { - name: "slots", - s: &slots, - wantRet: "0,1000-2000,5000-5200,5202-5232,5234-5399,5401-10000,16112-16113,16122-16123,16132,16153", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotRet := tt.s.String(); !reflect.DeepEqual(gotRet, tt.wantRet) { - t.Errorf("Slots.String() = %v, want %v", gotRet, tt.wantRet) - } - }) - } -} - -func TestSlots_Status(t *testing.T) { - slots := Slots{} - _ = slots.Set(0, SlotAssigned) - _ = slots.Set(100, SlotImporting) - _ = slots.Set(1, SlotMigrating) - _ = slots.Set(10000, SlotMigrating) - _ = slots.Set(10000, SlotUnAssigned) - _ = slots.Set(11000, SlotImporting) - _ = slots.Set(11000, SlotAssigned) - type args struct { - i int - } - - tests := []struct { - name string - s *Slots - args args - want SlotAssignStatus - }{ - { - name: "assigned", - s: &slots, - args: args{i: 0}, - want: SlotAssigned, - }, - { - name: "importing", - s: &slots, - args: args{i: 100}, - want: SlotImporting, - }, - { - name: "migrating", - s: &slots, - args: args{i: 1}, - want: SlotMigrating, - }, - { - name: "unassigned", - s: &slots, - args: args{i: 10000}, - want: SlotUnAssigned, - }, - { - name: "importing=>assigined", - s: &slots, - args: args{i: 11000}, - want: SlotAssigned, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Status(tt.args.i); got != tt.want { - t.Errorf("Slots.Status() = %v, want %v, %v", got, tt.want, tt.s) - } - }) - } -} - -func TestSlots_Sub(t *testing.T) { - var ( - slotsA = &Slots{} - slotsB = &Slots{} - slotsC = &Slots{} - - slotsDiff = &Slots{} - ) - - _ = slotsA.Set("0-1000", SlotImporting) - _ = slotsA.Set("999-2000", SlotAssigned) - _ = slotsA.Set("2001-3000", SlotMigrating) - _ = slotsA.Set("5001-6000", SlotUnAssigned) - - _ = slotsB.Set("0-1000", SlotAssigned) - _ = slotsB.Set("999-2000", SlotMigrating) - _ = slotsB.Set("2001-3000", SlotAssigned) - _ = slotsB.Set("5001-6000", SlotAssigned) - - _ = slotsC.Set("0-998", SlotMigrating) - _ = slotsC.Set("2000", SlotAssigned) - _ = slotsC.Set("2100", SlotAssigned) - _ = slotsC.Set("2101", SlotMigrating) - _ = slotsC.Set("2102", SlotImporting) - _ = slotsC.Set("2103", SlotUnAssigned) - _ = slotsC.Set("5001", SlotAssigned) - _ = slotsC.Set("5002", SlotImporting) - _ = slotsC.Set("5003", SlotMigrating) - - _ = slotsDiff.Set("999-1999,2001-2099,2102-3000", SlotAssigned) - - type args struct { - n *Slots - } - tests := []struct { - name string - s *Slots - args args - want *Slots - }{ - { - name: "no sub", - s: slotsA, - args: args{n: slotsB}, - want: &Slots{}, - }, - { - name: "sub", - s: slotsA, - args: args{n: slotsC}, - want: slotsDiff, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Sub(tt.args.n); !reflect.DeepEqual(got.data, tt.want.data) { - t.Errorf("Slots.Sub() = %v, want %v", got.String(), tt.want.String()) - } - }) - } -} - -func TestSlots_Load(t *testing.T) { - slots := NewSlots() - // slots.Load([]string{"1-100", "1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1", "77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca"}) - type args struct { - v interface{} - } - tests := []struct { - name string - slots *Slots - args args - wantErr bool - }{ - { - name: "load", - args: args{ - v: []string{"5000-5100", "1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1", "77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca"}, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := slots.Load(tt.args.v); (err != nil) != tt.wantErr { - t.Errorf("Slots.Load() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestSlots_Count(t *testing.T) { - slots := NewSlots() - _ = slots.Load([]string{"1-100", "1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1", "77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca"}) - - type args struct { - status SlotAssignStatus - } - tests := []struct { - name string - slots *Slots - args args - wantC int - }{ - { - name: "assigned", - slots: slots, - args: args{ - status: SlotAssigned, - }, - wantC: 100, - }, - { - name: "importing", - slots: slots, - args: args{ - status: SlotImporting, - }, - wantC: 1, - }, - { - name: "migrating", - slots: slots, - args: args{ - status: SlotMigrating, - }, - wantC: 1, - }, - { - name: "unassigned", - slots: slots, - args: args{ - status: SlotUnAssigned, - }, - wantC: 16284, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotC := tt.slots.Count(tt.args.status); gotC != tt.wantC { - t.Errorf("Slots.Count() = %v, want %v. %v", gotC, tt.wantC, tt.slots.String()) - } - }) - } -} diff --git a/cmd/redis-tools/pkg/types/user/user.go b/cmd/redis-tools/pkg/types/user/user.go deleted file mode 100644 index 4f9c629..0000000 --- a/cmd/redis-tools/pkg/types/user/user.go +++ /dev/null @@ -1,215 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package user - -import ( - "errors" - "fmt" - "regexp" - "strings" - - v1 "k8s.io/api/core/v1" -) - -const ( - // DefaultUserName from reids 6.0, there is a default user named "default" - // for compatibility, the default user set as RoleDeveloper - DefaultUserName = "default" - DefaultOperatorUserName = "operator" - - // password secret key name - PasswordSecretKey = "password" -) - -// Rule acl rules -// -// This rule supports redis 7.0, which is compatable with 6.0 -type Rule struct { - // Categories - Categories []string `json:"categories,omitempty"` - // AllowedCommands supports and | - AllowedCommands []string `json:"allowedCommands,omitempty"` - // DisallowedCommands supports and | - DisallowedCommands []string `json:"disallowedCommands,omitempty"` - // KeyPatterns support multi patterns, for 7.0 support %R~ and %W~ patterns - KeyPatterns []string `json:"keyPatterns,omitempty"` -} - -func (r *Rule) Validate() error { - if r == nil { - return errors.New("nil rule") - } - if len(r.Categories) == 0 && len(r.AllowedCommands) == 0 { - return errors.New("invalid rule, no allowed command") - } - if len(r.KeyPatterns) == 0 { - return errors.New("invalid rule, no key pattern") - } - return nil -} - -func (r *Rule) String() string { - return strings.Join(append(append(append(append([]string{}, r.Categories...), - r.AllowedCommands...), r.DisallowedCommands...), r.KeyPatterns...), " ") -} - -// UserRole -type UserRole string - -const ( - RoleOperator = "Operator" - RoleDeveloper = "Developer" -) - -// NewUser -func NewUser(name string, role UserRole, secret *v1.Secret) (*User, error) { - var ( - err error - passwd *Password - ) - if secret != nil { - if passwd, err = NewPassword(secret); err != nil { - return nil, err - } - } - if name == "" { - name = DefaultUserName - } - - user := &User{ - Name: name, - Role: role, - Password: passwd, - Rules: []*Rule{ - {Categories: []string{"all"}, KeyPatterns: []string{"*"}}, - }, - } - if err := user.Validate(); err != nil { - return nil, err - } - return user, nil -} - -// User -type User struct { - Name string `json:"name"` - Role UserRole `json:"role"` - Password *Password `json:"password,omitempty"` - Rules []*Rule `json:"rules,omitempty"` -} - -var ( - usernameReg = regexp.MustCompile(`^[0-9a-zA-Z-]{0,31}$`) -) - -// AppendRule -func (u *User) AppendRule(rules ...*Rule) error { - if u == nil { - return nil - } - for _, rule := range rules { - if err := rule.Validate(); err != nil { - return err - } - } - u.Rules = append(u.Rules, rules...) - return nil -} - -func (u *User) Validate() error { - if u == nil { - return fmt.Errorf("nil user") - } - if !usernameReg.MatchString(u.Name) { - return fmt.Errorf("invalid username which should match ^[0-9a-zA-Z-]{0,31}$") - } - - if u.Role != RoleOperator && u.Role != RoleDeveloper { - return fmt.Errorf(`unsupported user role "%s"`, u.Role) - } - return nil -} - -// String -func (u *User) String() string { - if u == nil { - return "" - } - - vals := []string{u.Name, string(u.Role)} - for _, rule := range u.Rules { - vals = append(vals, rule.String()) - } - return strings.Join(vals, " ") -} - -// Password -type Password struct { - SecretName string `json:"secretName,omitempty"` - secret *v1.Secret - data string -} - -// NewPassword -func NewPassword(secret *v1.Secret) (*Password, error) { - if secret == nil { - return nil, nil - } - - p := Password{} - if err := p.SetSecret(secret); err != nil { - return nil, err - } - return &p, nil -} - -func (p *Password) GetSecretName() string { - if p == nil { - return "" - } - return p.SecretName -} - -func (p *Password) SetSecret(secret *v1.Secret) error { - if p == nil || secret == nil { - return nil - } - - if val, ok := secret.Data[PasswordSecretKey]; !ok { - return fmt.Errorf("missing %s field for secret %s", PasswordSecretKey, secret.Name) - } else { - p.SecretName = secret.GetName() - p.secret = secret - p.data = string(val) - } - return nil -} - -func (p *Password) Secret() *v1.Secret { - if p == nil { - return nil - } - return p.secret -} - -// String return password in plaintext -func (p *Password) String() string { - if p == nil { - return "" - } - return p.data -} diff --git a/cmd/redis-tools/pkg/util/util.go b/cmd/redis-tools/pkg/util/util.go deleted file mode 100644 index 72180fd..0000000 --- a/cmd/redis-tools/pkg/util/util.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "crypto/tls" -) - -func LoadTLSConfig(tlsKeyFile, tlsCertFile string, skipverify bool) (*tls.Config, error) { - if tlsKeyFile != "" && tlsCertFile != "" { - cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) - if err != nil { - return nil, err - } - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: skipverify, - }, nil - } - return nil, nil -} diff --git a/cmd/redis-tools/scripts/init.sh b/cmd/redis-tools/scripts/init.sh deleted file mode 100755 index 55f541c..0000000 --- a/cmd/redis-tools/scripts/init.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -chmod -f 644 /data/*.rdb /data/*.aof /data/*.conf || true -chown -f 999:1000 /data/*.rdb /data/*.aof /data/*.conf || true - - -if [[ "$NODEPORT_ENABLED" = "true" ]] || [[ ! -z "$IP_FAMILY_PREFER" ]] ; then - echo "nodeport expose" - /opt/redis-tools cluster expose || exit 1 -fi - -# copy binaries -cp /opt/* /mnt/opt/ && chmod 555 /mnt/opt/* diff --git a/cmd/redis-tools/scripts/run.sh b/cmd/redis-tools/scripts/run.sh deleted file mode 100755 index 593010f..0000000 --- a/cmd/redis-tools/scripts/run.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/sh - -export PATH=/opt:$PATH - -REDIS_CONFIG="/tmp/redis.conf" -ANNOUNCE_CONFIG="/data/announce.conf" -CLUSTER_CONFIG="/data/nodes.conf" -OPERATOR_PASSWORD_FILE="/account/password" -TLS_DIR="/tls" - -echo "# Run: cluster heal" -/opt/redis-tools cluster heal || exit 1 - -if [[ -f ${CLUSTER_CONFIG} ]]; then - if [[ -z "${POD_IP}" ]]; then - echo "Unable to determine Pod IP address!" - exit 1 - fi - echo "Updating my IP to ${POD_IP} in ${CLUSTER_CONFIG}" - sed -i.bak -e "/myself/ s/ .*:[0-9]*@[0-9]*/ ${POD_IP}:6379@16379/" ${CLUSTER_CONFIG} -fi - -cat /conf/redis.conf > ${REDIS_CONFIG} - -password=$(cat ${OPERATOR_PASSWORD_FILE}) - -# when redis acl supported, inject acl config -if [[ ! -z "${ACL_CONFIGMAP_NAME}" ]]; then - echo "# Run: generate acl " - /opt/redis-tools helper generate acl --name ${ACL_CONFIGMAP_NAME} --namespace ${NAMESPACE} >> ${REDIS_CONFIG} || exit 1 -fi - -if [[ "${ACL_ENABLED}" = "true" ]]; then - if [ ! -z "${OPERATOR_USERNAME}" ]; then - echo "masteruser \"${OPERATOR_USERNAME}\"" >> ${REDIS_CONFIG} - fi - if [ ! -z "${password}" ]; then - echo "masterauth \"${password}\"" >> ${REDIS_CONFIG} - fi -elif [[ ! -z "${password}" ]]; then - echo "masterauth \"${password}\"" >> ${REDIS_CONFIG} - echo "requirepass \"${password}\"" >> ${REDIS_CONFIG} -fi - -if [[ -f ${ANNOUNCE_CONFIG} ]]; then - echo "append announce conf to redis config" - cat ${ANNOUNCE_CONFIG} >> ${REDIS_CONFIG} -fi - -POD_IPS_LIST=$(echo "${POD_IPS}"|sed 's/,/ /g') - -ARGS="--cluster-enabled yes --cluster-config-file ${CLUSTER_CONFIG} --protected-mode no" - -if [ ! -z "${POD_IPS}" ]; then - if [[ "${IP_FAMILY_PREFER}" = "IPv6" ]]; then - POD_IPv6=$(echo ${POD_IPS_LIST} |grep -E '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))' -o -C 1) - ARGS="${ARGS} --bind ${POD_IPv6} ::1" - else - ARGS="${ARGS} --bind ${POD_IPS_LIST} localhost" - fi -fi - - -if [[ "${TLS_ENABLED}" = "true" ]]; then - ARGS="${ARGS} --port 0 --tls-port 6379 --tls-cluster yes --tls-replication yes --tls-cert-file ${TLS_DIR}/tls.crt --tls-key-file ${TLS_DIR}/tls.key --tls-ca-cert-file ${TLS_DIR}/ca.crt" -fi - -chmod 0640 ${REDIS_CONFIG} - -redis-server ${REDIS_CONFIG} ${ARGS} $@ diff --git a/cmd/redis-tools/sync/controller.go b/cmd/redis-tools/sync/controller.go new file mode 100644 index 0000000..19c3bd6 --- /dev/null +++ b/cmd/redis-tools/sync/controller.go @@ -0,0 +1,273 @@ +package sync + +import ( + "context" + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/go-logr/logr" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/retry" +) + +type PersistentObject struct { + data map[string][]byte +} + +func LoadPersistentObject(ctx context.Context, client *kubernetes.Clientset, kind, namespace, name string) (*PersistentObject, error) { + switch strings.ToLower(kind) { + case "secret": + secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + return &PersistentObject{data: map[string][]byte{}}, nil + } else if err != nil { + return nil, err + } + return &PersistentObject{data: secret.Data}, nil + default: + cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + return &PersistentObject{data: map[string][]byte{}}, nil + } else if err != nil { + return nil, err + } + obj := &PersistentObject{data: map[string][]byte{}} + for k, v := range cm.Data { + obj.data[k] = []byte(v) + } + return obj, nil + } +} + +func NewObject() *PersistentObject { + return &PersistentObject{data: map[string][]byte{}} +} + +func (obj *PersistentObject) Get(key string) []byte { + if obj == nil { + return nil + } + return obj.data[key] +} + +func (obj *PersistentObject) Set(key string, val []byte) { + if obj == nil { + return + } + if obj.data == nil { + obj.data = map[string][]byte{} + } + obj.data[key] = val +} + +func (obj *PersistentObject) Save(ctx context.Context, client *kubernetes.Clientset, kind, namespace, name string, + owners []metav1.OwnerReference, logger logr.Logger) error { + + switch strings.ToLower(kind) { + case "secret": + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + OwnerReferences: owners, + }, + Data: obj.data, + } + + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldSecret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil { + logger.Error(err, "create secret failed") + return err + } + } else if err != nil { + logger.Error(err, "get secret failed", "target", name) + return err + } + secret.ResourceVersion = oldSecret.ResourceVersion + if _, err := client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "update secret failed", "target", name) + return err + } + return nil + }) + default: + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + OwnerReferences: owners, + }, + Data: map[string]string{}, + } + for k, v := range obj.data { + cm.Data[k] = string(v) + } + + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldCm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + if _, err := client.CoreV1().ConfigMaps(namespace).Create(ctx, cm, metav1.CreateOptions{}); err != nil { + logger.Error(err, "create configmap failed") + return err + } + } else if err != nil { + logger.Error(err, "get configmap failed", "target", name) + return err + } + cm.ResourceVersion = oldCm.ResourceVersion + if _, err := client.CoreV1().ConfigMaps(namespace).Update(ctx, cm, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "update configmap failed", "target", name) + return err + } + return nil + }) + } +} + +type UpdateEvent struct { + FilePath string + Data string + Timestamp int64 +} + +type Filter interface { + Truncate(filename string, data string) (string, error) +} + +type ControllerOptions struct { + ResourceKind string + Namespace string + Name string + OwnerReferences []metav1.OwnerReference + SyncInterval time.Duration + Filters []Filter +} + +// Controller +type Controller struct { + client *kubernetes.Clientset + + dataUpdateChan chan *UpdateEvent + filters []Filter + options ControllerOptions + logger logr.Logger +} + +func NewController(client *kubernetes.Clientset, options ControllerOptions, logger logr.Logger) (*Controller, error) { + logger = logger.WithName("Controller") + + if options.SyncInterval <= time.Second*3 { + options.SyncInterval = time.Second * 10 + } + if options.Namespace == "" { + return nil, fmt.Errorf("namespace required") + } + if options.Name == "" { + return nil, fmt.Errorf("configmap name required") + } + if len(options.OwnerReferences) == 0 { + logger.Info("WARNING: resource owner reference not specified") + } + + ctrl := Controller{ + client: client, + dataUpdateChan: make(chan *UpdateEvent, 100), + options: options, + filters: options.Filters, + logger: logger, + } + return &ctrl, nil +} + +func (c *Controller) Handler(ctx context.Context, fs *FileStat) error { + logger := c.logger.WithName("Handler") + + data, err := os.ReadFile(fs.FilePath()) + if err != nil { + logger.Error(err, "load file content failed", "filepath", fs.FilePath()) + return err + } + + select { + case <-ctx.Done(): + return ctx.Err() + case c.dataUpdateChan <- &UpdateEvent{FilePath: fs.FilePath(), Data: string(data)}: + } + return nil +} + +func (c *Controller) Run(ctx context.Context) error { + logger := c.logger + + obj, err := LoadPersistentObject(ctx, c.client, c.options.ResourceKind, c.options.Namespace, c.options.Name) + if err != nil { + logger.Error(err, "load object failed", "kind", c.options.ResourceKind, "namespace", c.options.Namespace, "name", c.options.Name) + return err + } + + ticker := time.NewTicker(c.options.SyncInterval) + defer ticker.Stop() + + isChanged := int64(0) + for { + select { + case <-ctx.Done(): + return nil + case event, ok := <-c.dataUpdateChan: + if !ok { + return nil + } + + oldData := obj.Get(event.FilePath) + oldSize := len(oldData) + + // etcd limiteds configmap/secret size limit to 1Mi + if oldSize-len(oldData)+len(event.Data) >= 1024*1024*1024-4096 { + logger.Error(fmt.Errorf("data size has exceed 1Mi"), "filepath", event.FilePath) + continue + } + + if event.Data == string(oldData) { + continue + } + + filename := path.Base(event.FilePath) + if func() bool { + for _, filter := range c.filters { + if data, err := filter.Truncate(filename, event.Data); err != nil { + return false + } else { + event.Data = data + } + } + return true + }() { + obj.Set(filename, []byte(event.Data)) + isChanged += 1 + } + case <-ticker.C: + if isChanged == 0 { + continue + } + + func() { + ctx, cancel := context.WithTimeout(ctx, c.options.SyncInterval-time.Second) + defer cancel() + + if err := obj.Save(ctx, c.client, c.options.ResourceKind, + c.options.Namespace, c.options.Name, c.options.OwnerReferences, logger); err != nil { + logger.Error(err, "update configmap failed") + } + }() + isChanged = 0 + } + } +} diff --git a/cmd/redis-tools/pkg/sync/file.go b/cmd/redis-tools/sync/file.go similarity index 85% rename from cmd/redis-tools/pkg/sync/file.go rename to cmd/redis-tools/sync/file.go index 076ecf8..04eb22a 100644 --- a/cmd/redis-tools/pkg/sync/file.go +++ b/cmd/redis-tools/sync/file.go @@ -1,19 +1,3 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package sync import ( diff --git a/cmd/redis-tools/sync/redis.go b/cmd/redis-tools/sync/redis.go new file mode 100644 index 0000000..a47204c --- /dev/null +++ b/cmd/redis-tools/sync/redis.go @@ -0,0 +1,14 @@ +package sync + +import ( + "errors" +) + +type RedisClusterFilter struct{} + +func (v *RedisClusterFilter) Truncate(filename string, data string) (string, error) { + if len(data) == 0 { + return "", errors.New("empty data") + } + return data, nil +} diff --git a/cmd/redis-tools/util/client.go b/cmd/redis-tools/util/client.go new file mode 100644 index 0000000..80182bc --- /dev/null +++ b/cmd/redis-tools/util/client.go @@ -0,0 +1,43 @@ +package util + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" +) + +func NewClient() (*kubernetes.Clientset, error) { + var ( + err error + conf *rest.Config + ) + + host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") + if host == "" && port == "" { + if fp := os.Getenv("KUBE_CONFIG"); fp != "" { + if conf, err = clientcmd.BuildConfigFromFlags("", fp); err != nil { + return nil, fmt.Errorf("load config from $KUBE_CONFIG failed, error=%s", err) + } + } else { + if home := homedir.HomeDir(); home != "" { + fp := filepath.Join(home, ".kube", "config") + if conf, err = clientcmd.BuildConfigFromFlags("", fp); err != nil { + return nil, fmt.Errorf("load config from local .kube/config failed, error=%s", err) + } + } else { + return nil, fmt.Errorf("no local config found") + } + } + } else { + conf, err = rest.InClusterConfig() + if err != nil { + return nil, err + } + } + return kubernetes.NewForConfig(conf) +} diff --git a/cmd/redis-tools/pkg/logger/logger.go b/cmd/redis-tools/util/logger.go similarity index 53% rename from cmd/redis-tools/pkg/logger/logger.go rename to cmd/redis-tools/util/logger.go index 22e92db..d8c5f60 100644 --- a/cmd/redis-tools/pkg/logger/logger.go +++ b/cmd/redis-tools/util/logger.go @@ -1,20 +1,4 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package logger +package util import ( "github.com/go-logr/logr" diff --git a/cmd/redis-tools/util/util.go b/cmd/redis-tools/util/util.go new file mode 100644 index 0000000..04332a3 --- /dev/null +++ b/cmd/redis-tools/util/util.go @@ -0,0 +1,19 @@ +package util + +import ( + "crypto/tls" +) + +func LoadTLSConfig(tlsKeyFile, tlsCertFile string, skipverify bool) (*tls.Config, error) { + if tlsKeyFile != "" && tlsCertFile != "" { + cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) + if err != nil { + return nil, err + } + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: skipverify, // #nosec G402 + }, nil + } + return nil, nil +} diff --git a/config/crd/bases/databases.spotahome.com_redisfailovers.yaml b/config/crd/bases/databases.spotahome.com_redisfailovers.yaml index 0be06c9..51b935f 100644 --- a/config/crd/bases/databases.spotahome.com_redisfailovers.yaml +++ b/config/crd/bases/databases.spotahome.com_redisfailovers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.16.0 name: redisfailovers.databases.spotahome.com spec: group: databases.spotahome.com @@ -15,36 +15,49 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - description: Master address - jsonPath: .status.master.address - name: Master + - description: Redis replicas + jsonPath: .spec.redis.replicas + name: Replicas + type: integer + - description: Redis sentinel replicas + jsonPath: .spec.sentinel.replicas + name: Sentinels + type: integer + - description: Instance access type + jsonPath: .spec.redis.expose.type + name: Access type: string - - description: Master status - jsonPath: .status.master.status - name: Master Status - type: string - - description: Instance reconcile phase + - description: Instance phase jsonPath: .status.phase - name: Phase + name: Status type: string - - description: Status message - jsonPath: .status.reason + - description: Instance status message + jsonPath: .status.message name: Message type: string + - description: Time since creation + jsonPath: .metadata.creationTimestamp + name: Age + type: date name: v1 schema: openAPIV3Schema: description: RedisFailover is the Schema for the redisfailovers API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -52,74 +65,44 @@ spec: description: RedisFailoverSpec represents a Redis failover spec properties: auth: - description: Auth default user auth settings + description: Auth properties: secretPath: - description: SecretName is the name of the secret containing the - auth credentials. type: string type: object - enableTLS: - description: EnableTLS enable TLS for Redis + enableActiveRedis: + description: EnableActiveRedis enable active-active model for Redis type: boolean - expose: - description: Expose service access configuration - properties: - accessPort: - description: AccessPort lb service access port - format: int32 - type: integer - dataStorageNodePortMap: - additionalProperties: - format: int32 - type: integer - description: DataStorageNodePortMap redis port map referred by - pod name - type: object - dataStorageNodePortSequence: - description: DataStorageNodePortSequence redis port list separated - by commas - type: string - enableNodePort: - description: EnableNodePort enable nodeport - type: boolean - exposeImage: - description: ExposeImage expose image - type: string - type: object labelWhitelist: - description: LabelWhitelist is a list of label names that are allowed - to be present on a pod items: type: string type: array redis: - description: Redis redis data node settings + description: RedisSettings defines the specification of the redis + cluster properties: affinity: - description: Affinity is the affinity settings for the Redis pods. + description: Affinity is a group of affinity scheduling rules. properties: nodeAffinity: description: Describes node affinity scheduling rules for the pod. properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node matches the corresponding matchExpressions; - the node(s) with the highest sum are the most preferred. + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects - (i.e. is also a no-op). + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). properties: preference: description: A node selector term, associated with @@ -129,32 +112,26 @@ spec: description: A list of node selector requirements by node's labels. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -167,32 +144,26 @@ spec: description: A list of node selector requirements by node's fields. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -214,53 +185,46 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from - its node. + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. properties: nodeSelectorTerms: description: Required. A list of node selector terms. The terms are ORed. items: - description: A null or empty node selector term - matches no objects. The requirements of them are - ANDed. The TopologySelectorTerm type implements - a subset of the NodeSelectorTerm. + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. properties: matchExpressions: description: A list of node selector requirements by node's labels. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -273,32 +237,26 @@ spec: description: A list of node selector requirements by node's fields. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -321,18 +279,16 @@ spec: other pod(s)). properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. items: description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred @@ -351,30 +307,25 @@ spec: of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -386,53 +337,45 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -444,42 +387,37 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. Empty topologyKey is not allowed. type: string required: - topologyKey type: object weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. format: int32 type: integer required: @@ -488,23 +426,22 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to a pod label update), - the system may or may not try to eventually evict the - pod from its node. When there are multiple elements, - the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running properties: labelSelector: description: A label query over a set of resources, @@ -515,28 +452,24 @@ spec: selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -549,51 +482,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -606,33 +532,29 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. type: string required: - topologyKey @@ -645,18 +567,16 @@ spec: as some other pod(s)). properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the anti-affinity expressions - specified by this field, but it may choose a node that - violates one or more of the expressions. The node that - is most preferred is the one with the greatest sum of - weights, i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - anti-affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. items: description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred @@ -675,30 +595,25 @@ spec: of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -710,53 +625,45 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -768,42 +675,37 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. Empty topologyKey is not allowed. type: string required: - topologyKey type: object weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. format: int32 type: integer required: @@ -812,23 +714,22 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified - by this field are not met at scheduling time, the pod - will not be scheduled onto the node. If the anti-affinity - requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod - label update), the system may or may not try to eventually - evict the pod from its node. When there are multiple - elements, the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running properties: labelSelector: description: A label query over a set of resources, @@ -839,28 +740,24 @@ spec: selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -873,51 +770,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -930,33 +820,29 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. type: string required: - topologyKey @@ -965,60 +851,44 @@ spec: type: object type: object backup: - description: Backup schedule backup configuration. + description: RedisBackup defines the structure used to backup + the Redis Data properties: image: - description: Image is the Redis backup image to run. type: string schedule: - description: Schedule is the backup schedule. items: - description: Schedule properties: keep: - description: Keep is the number of backups to keep. format: int32 - minimum: 1 type: integer keepAfterDeletion: - description: KeepAfterDeletion is the flag to keep the - data after the RedisFailover is deleted. type: boolean name: - description: Name is the scheduled backup name. type: string schedule: - description: Schedule crontab like schedule. type: string storage: - description: Storage is the backup storage configuration. properties: size: anyOf: - type: integer - type: string - description: Size is the size of the PersistentVolumeClaim. pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true storageClassName: - description: StorageClassName is the name of the - StorageClass to use for the PersistentVolumeClaim. type: string type: object target: - description: Target is the backup target configuration. properties: s3Option: - description: S3Option is the S3 backup target configuration. + description: S3Option properties: bucket: - description: Bucket s3 storage bucket type: string dir: - description: Dir s3 storage dir type: string s3Secret: - description: S3Secret s3 storage access secret type: string type: object type: object @@ -1032,91 +902,179 @@ spec: customConfig: additionalProperties: type: string - description: CustomConfig is a map of custom Redis configuration - options. + description: CustomConfig custom redis configuration type: object - dnsPolicy: - description: DNSPolicy is the DNS policy for the Redis pods. - type: string + enableTLS: + description: EnableTLS enable TLS for Redis + type: boolean exporter: - description: Exporter prometheus exporter settings. + description: Exporter properties: enabled: - description: Enabled is the flag to enable redis exporter type: boolean image: - description: Image exporter image type: string imagePullPolicy: - description: ImagePullPolicy image pull policy. + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + expose: + description: Expose + properties: + accessPort: + description: AccessPort defines the lb access nodeport + format: int32 + type: integer + annotations: + additionalProperties: + type: string + description: The annnotations of the service which attached + to services + type: object + dataStorageNodePortMap: + additionalProperties: + format: int32 + type: integer + description: |- + NodePortMap defines the map of the nodeport for redis sentinel only + Reversed for 3.14 backward compatibility + type: object + dataStorageNodePortSequence: + description: |- + NodePortMap defines the map of the nodeport for redis nodes + NodePortSequence defines the sequence of the nodeport for redis cluster only + type: string + image: + description: Image defines the image used to expose redis + from annotations + type: string + imagePullPolicy: + description: ImagePullPolicy defines the image pull policy + type: string + ipFamilyPrefer: + description: IPFamily represents the IP Family (IPv4 or IPv6). + This type is used to express the family of an IP expressed + by a type (e.g. service.spec.ipFamilies). + enum: + - IPv4 + - IPv6 + type: string + type: + description: ServiceType defines the type of the all related + service + enum: + - NodePort + - LoadBalancer + - ClusterIP type: string type: object - hostNetwork: - description: HostNetwork is the host network settings for the - Redis pods. - type: boolean image: - description: Image is the Redis image to run. type: string imagePullPolicy: - description: ImagePullPolicy is the Image pull policy. + description: PullPolicy describes a policy for if/when to pull + a container image type: string imagePullSecrets: - description: ImagePullSecrets is the list of secrets used to pull - the Redis image from a private registry. items: - description: LocalObjectReference contains enough information - to let you locate the referenced object inside the same namespace. + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. properties: name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string type: object x-kubernetes-map-type: atomic type: array - ipFamilyPrefer: - description: IPFamily represents the IP Family (IPv4 or IPv6). - This type is used to express the family of an IP expressed by - a type (e.g. service.spec.ipFamilies). - enum: - - IPv4 - - IPv6 - type: string nodeSelector: additionalProperties: type: string - description: NodeSelector is the node selector for the Redis pods. type: object podAnnotations: additionalProperties: type: string - description: PodAnnotations is the annotations for the Redis pods. type: object replicas: - description: Replicas is the number of Redis replicas to run. format: int32 - maximum: 5 - minimum: 0 type: integer resources: - description: Resources is the resource requirements for the Redis - container. + description: ResourceRequirements describes the compute resource + requirements. properties: claims: - description: "Claims lists the names of resources, defined - in spec.resourceClaims, that are used by this container. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: Name must match the name of one entry in - pod.spec.resourceClaims of the Pod where this field - is used. It makes that resource available inside a - container. + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. type: string required: - name @@ -1132,8 +1090,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -1142,88 +1101,93 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed - Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object restore: - description: Restore restore redis instance from backup. + description: RedisBackup defines the structure used to restore + the Redis Data properties: backupName: - description: BackupName is the backup cr name to restore. type: string image: - description: Image is the Redis restore image to run. type: string imagePullPolicy: - description: ImagePullPolicy is the Image pull policy. + description: PullPolicy describes a policy for if/when to + pull a container image type: string type: object securityContext: - description: SecurityContext is the security context for the Redis - pods. + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. properties: fsGroup: - description: "A special supplemental group that applies to - all containers in a pod. Some volume types allow the Kubelet - to change the ownership of that volume to be owned by the - pod: \n 1. The owning GID will be the FSGroup 2. The setgid - bit is set (new files created in the volume will be owned - by FSGroup) 3. The permission bits are OR'd with rw-rw---- - \n If unset, the Kubelet will not modify the ownership and - permissions of any volume. Note that this field cannot be - set when spec.os.name is windows." + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer fsGroupChangePolicy: - description: 'fsGroupChangePolicy defines behavior of changing - ownership and permission of the volume before being exposed - inside Pod. This field will only apply to volume types which - support fsGroup based ownership(and permissions). It will - have no effect on ephemeral volume types such as: secret, - configmaps and emptydir. Valid values are "OnRootMismatch" - and "Always". If not specified, "Always" is used. Note that - this field cannot be set when spec.os.name is windows.' + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. type: string runAsGroup: - description: The GID to run the entrypoint of the container - process. Uses runtime default if unset. May also be set - in SecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext - takes precedence for that container. Note that this field - cannot be set when spec.os.name is windows. + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer runAsNonRoot: - description: Indicates that the container must run as a non-root - user. If true, the Kubelet will validate the image at runtime - to ensure that it does not run as UID 0 (root) and fail - to start the container if it does. If unset or false, no - such validation will be performed. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: boolean runAsUser: - description: The UID to run the entrypoint of the container - process. Defaults to user specified in image metadata if - unspecified. May also be set in SecurityContext. If set - in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. - Note that this field cannot be set when spec.os.name is - windows. + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer seLinuxOptions: - description: The SELinux context to be applied to all containers. - If unspecified, the container runtime will allocate a random - SELinux context for each container. May also be set in - SecurityContext. If set in both SecurityContext and PodSecurityContext, - the value specified in SecurityContext takes precedence - for that container. Note that this field cannot be set when - spec.os.name is windows. + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. properties: level: description: Level is SELinux level label that applies @@ -1243,46 +1207,47 @@ spec: type: string type: object seccompProfile: - description: The seccomp options to use by the containers - in this pod. Note that this field cannot be set when spec.os.name - is windows. + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. properties: localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. The profile must - be preconfigured on the node to work. Must be a descending - path, relative to the kubelet's configured seccomp profile - location. Must only be set if type is "Localhost". + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. type: string type: - description: "type indicates which kind of seccomp profile - will be applied. Valid options are: \n Localhost - a - profile defined in a file on the node should be used. - RuntimeDefault - the container runtime default profile - should be used. Unconfined - no profile should be applied." + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. type: string required: - type type: object supplementalGroups: - description: A list of groups applied to the first process - run in each container, in addition to the container's primary - GID, the fsGroup (if specified), and group memberships defined - in the container image for the uid of the container process. - If unspecified, no additional groups are added to any container. - Note that group memberships defined in the container image - for the uid of the container process are still effective, - even if they are not included in this list. Note that this - field cannot be set when spec.os.name is windows. + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. items: format: int64 type: integer type: array sysctls: - description: Sysctls hold a list of namespaced sysctls used - for the pod. Pods with unsupported sysctls (by the container - runtime) might fail to launch. Note that this field cannot - be set when spec.os.name is windows. + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. items: description: Sysctl defines a kernel parameter to be set properties: @@ -1298,107 +1263,126 @@ spec: type: object type: array windowsOptions: - description: The Windows specific settings applied to all - containers. If unspecified, the options within a container's - SecurityContext will be used. If set in both SecurityContext - and PodSecurityContext, the value specified in SecurityContext - takes precedence. Note that this field cannot be set when - spec.os.name is linux. + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. properties: gmsaCredentialSpec: - description: GMSACredentialSpec is where the GMSA admission - webhook (https://github.com/kubernetes-sigs/windows-gmsa) - inlines the contents of the GMSA credential spec named - by the GMSACredentialSpecName field. + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. type: string gmsaCredentialSpecName: description: GMSACredentialSpecName is the name of the GMSA credential spec to use. type: string hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. This field is - alpha-level and will only be honored by components that - enable the WindowsHostProcessContainers feature flag. - Setting this field without the feature flag will result - in errors when validating the Pod. All of a Pod's containers - must have the same effective HostProcess value (it is - not allowed to have a mix of HostProcess containers - and non-HostProcess containers). In addition, if HostProcess - is true then HostNetwork must also be set to true. + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. type: boolean runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. Defaults to the user specified - in image metadata if unspecified. May also be set in - PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext - takes precedence. + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: string type: object type: object serviceAnnotations: additionalProperties: type: string - description: ServiceAnnotations is the annotations for the Redis - service. type: object storage: - description: Storage redis data persistence settings. + description: RedisStorage defines the structure used to store + the Redis Data properties: + emptyDir: + description: |- + Represents an empty directory for a pod. + Empty directory volumes support ownership management and SELinux relabeling. + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object keepAfterDeletion: - description: KeepAfterDeletion is the flag to keep the data - after the RedisFailover is deleted. type: boolean persistentVolumeClaim: - description: PersistentVolumeClaim is the PVC volume source. + description: PersistentVolumeClaim is a user's request for + and claim to a persistent volume properties: apiVersion: - description: 'APIVersion defines the versioned schema - of this representation of an object. Servers should - convert recognized schemas to the latest internal value, - and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the - REST resource this object represents. Servers may infer - this from the endpoint the client submits requests to. - Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: - description: 'Standard object''s metadata. More info: - https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata type: object spec: - description: 'spec defines the desired characteristics - of a volume requested by a pod author. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims properties: accessModes: - description: 'accessModes contains the desired access - modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 items: type: string type: array dataSource: - description: 'dataSource field can be used to specify - either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) If the - provisioner or an external controller can support - the specified data source, it will create a new - volume based on the contents of the specified data - source. When the AnyVolumeDataSource feature gate - is enabled, dataSource contents will be copied to - dataSourceRef, and dataSourceRef contents will be - copied to dataSource when dataSourceRef.namespace - is not specified. If the namespace is specified, - then dataSourceRef will not be copied to dataSource.' + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. properties: apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. type: string kind: description: Kind is the type of resource being @@ -1414,42 +1398,36 @@ spec: type: object x-kubernetes-map-type: atomic dataSourceRef: - description: 'dataSourceRef specifies the object from - which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a - non-empty API group (non core object) or a PersistentVolumeClaim - object. When this field is specified, volume binding - will only succeed if the type of the specified object - matches some installed volume populator or dynamic - provisioner. This field will replace the functionality - of the dataSource field and as such if both fields - are non-empty, they must have the same value. For - backwards compatibility, when namespace isn''t specified - in dataSourceRef, both fields (dataSource and dataSourceRef) - will be set to the same value automatically if one - of them is empty and the other is non-empty. When - namespace is specified in dataSourceRef, dataSource - isn''t set to the same value and must be empty. - There are three important differences between dataSource - and dataSourceRef: * While dataSource only allows - two specific types of objects, dataSourceRef allows - any non-core object, as well as PersistentVolumeClaim - objects. * While dataSource ignores disallowed values - (dropping them), dataSourceRef preserves all values, - and generates an error if a disallowed value is - specified. * While dataSource only allows local - objects, dataSourceRef allows objects in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource - feature gate to be enabled. (Alpha) Using the namespace - field of dataSourceRef requires the CrossNamespaceVolumeDataSource - feature gate to be enabled.' + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. properties: apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. type: string kind: description: Kind is the type of resource being @@ -1460,44 +1438,41 @@ spec: referenced type: string namespace: - description: Namespace is the namespace of resource - being referenced Note that when a namespace - is specified, a gateway.networking.k8s.io/ReferenceGrant - object is required in the referent namespace - to allow that namespace's owner to accept the - reference. See the ReferenceGrant documentation - for details. (Alpha) This field requires the - CrossNamespaceVolumeDataSource feature gate - to be enabled. + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. type: string required: - kind - name type: object resources: - description: 'resources represents the minimum resources - the volume should have. If RecoverVolumeExpansionFailure - feature is enabled users are allowed to specify - resource requirements that are lower than previous - value but must still be higher than capacity recorded - in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources properties: claims: - description: "Claims lists the names of resources, - defined in spec.resourceClaims, that are used - by this container. \n This is an alpha field - and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It - can only be set for containers." + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: Name must match the name of - one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes - that resource available inside a container. + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. type: string required: - name @@ -1513,8 +1488,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount - of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -1523,12 +1499,11 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount - of compute resources required. If Requests is - omitted for a container, it defaults to Limits - if that is explicitly specified, otherwise to - an implementation-defined value. Requests cannot - exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object selector: @@ -1540,26 +1515,25 @@ spec: selector requirements. The requirements are ANDed. items: - description: A label selector requirement is - a selector that contains values, a key, and - an operator that relates the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If - the operator is Exists or DoesNotExist, - the values array must be empty. This array - is replaced during a strategic merge patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -1571,23 +1545,22 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic storageClassName: - description: 'storageClassName is the name of the - StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 type: string volumeMode: - description: volumeMode defines what type of volume - is required by the claim. Value of Filesystem is - implied when not included in claim spec. + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. type: string volumeName: description: volumeName is the binding reference to @@ -1595,17 +1568,66 @@ spec: type: string type: object status: - description: 'status represents the current information/status - of a persistent volume claim. Read-only. More info: - https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims properties: accessModes: - description: 'accessModes contains the actual access - modes the volume backing the PVC has. More info: - https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 items: type: string type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey + names follow standard Kubernetes label syntax. Valid + values are either:\n\t* Un-prefixed keys:\n\t\t- + storage - the capacity of the volume.\n\t* Custom + resources must use implementation-defined prefixed + names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or + have kubernetes.io prefix are considered\nreserved + and hence may not be used.\n\nClaimResourceStatus + can be in any of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller + with a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing + the volume but further resizing of\n\t\tvolume is + needed on the node.\n\t- NodeResizeInProgress:\n\t\tState + set when kubelet starts resizing the volume.\n\t- + NodeResizeFailed:\n\t\tState set when resizing has + failed in kubelet with a terminal error. Transient + errors don't set\n\t\tNodeResizeFailed.\nFor example: + if expanding a PVC for more capacity - this field + can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress + for the given PVC.\n\nA controller that receives + PVC update with previously unknown resourceName + or ClaimResourceStatus\nshould ignore the update + for the purpose it was designed. For example - a + controller that\nonly is responsible for resizing + capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with + PVC.\n\nThis is an alpha field and requires enabling + RecoverVolumeExpansionFailure feature." + type: object + x-kubernetes-map-type: granular allocatedResources: additionalProperties: anyOf: @@ -1613,20 +1635,33 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: allocatedResources is the storage resource - within AllocatedResources tracks the capacity allocated - to a PVC. It may be larger than the actual capacity - when a volume expansion operation is requested. - For storage quota, the larger value from allocatedResources - and PVC.spec.resources is used. If allocatedResources + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey + names follow standard Kubernetes label syntax. Valid + values are either:\n\t* Un-prefixed keys:\n\t\t- + storage - the capacity of the volume.\n\t* Custom + resources must use implementation-defined prefixed + names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or + have kubernetes.io prefix are considered\nreserved + and hence may not be used.\n\nCapacity reported + here may be larger than the actual capacity when + a volume expansion operation\nis requested.\nFor + storage quota, the larger value from allocatedResources + and PVC.spec.resources is used.\nIf allocatedResources is not set, PVC.spec.resources alone is used for - quota calculation. If a volume expansion capacity - request is lowered, allocatedResources is only lowered + quota calculation.\nIf a volume expansion capacity + request is lowered, allocatedResources is only\nlowered if there are no expansion operations in progress - and if the actual volume capacity is equal or lower - than the requested capacity. This is an alpha field - and requires enabling RecoverVolumeExpansionFailure - feature. + and if the actual volume capacity\nis equal or lower + than the requested capacity.\n\nA controller that + receives PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. + For example - a controller that\nonly is responsible + for resizing capacity of the volume, should ignore + PVC updates that change other valid\nresources associated + with PVC.\n\nThis is an alpha field and requires + enabling RecoverVolumeExpansionFailure feature." type: object capacity: additionalProperties: @@ -1639,10 +1674,9 @@ spec: of the underlying volume. type: object conditions: - description: conditions is the current Condition of - persistent volume claim. If underlying persistent - volume is being resized then the Condition will - be set to 'ResizeStarted'. + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. items: description: PersistentVolumeClaimCondition contains details about state of pvc @@ -1663,12 +1697,10 @@ spec: indicating details about last transition. type: string reason: - description: reason is a unique, this should - be a short, machine understandable string - that gives the reason for condition's last - transition. If it reports "ResizeStarted" - that means the underlying persistent volume - is being resized. + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. type: string status: type: string @@ -1685,87 +1717,74 @@ spec: description: phase represents the current phase of PersistentVolumeClaim. type: string - resizeStatus: - description: resizeStatus stores status of resize - operation. ResizeStatus is not set by default but - when expansion is complete resizeStatus is set to - empty string by resize controller or kubelet. This - is an alpha field and requires enabling RecoverVolumeExpansionFailure - feature. - type: string type: object type: object type: object tolerations: - description: Tolerations is the list of tolerations for the Redis - pods. items: - description: The pod this Toleration is attached to tolerates - any taint that matches the triple using - the matching operator . + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . properties: effect: - description: Effect indicates the taint effect to match. - Empty means match all taint effects. When specified, allowed - values are NoSchedule, PreferNoSchedule and NoExecute. + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. type: string key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match - all values and all keys. + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. type: string operator: - description: Operator represents a key's relationship to - the value. Valid operators are Exists and Equal. Defaults - to Equal. Exists is equivalent to wildcard for value, - so that a pod can tolerate all taints of a particular - category. + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. type: string tolerationSeconds: - description: TolerationSeconds represents the period of - time the toleration (which must be of effect NoExecute, - otherwise this field is ignored) tolerates the taint. - By default, it is not set, which means tolerate the taint - forever (do not evict). Zero and negative values will - be treated as 0 (evict immediately) by the system. + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. format: int64 type: integer value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. type: string type: object type: array type: object sentinel: - description: Sentinel sentinel node settings + description: SentinelSettings defines the specification of the sentinel + cluster properties: affinity: - description: Affinity is the affinity settings for the Redis pods. + description: Affinity is a group of affinity scheduling rules. properties: nodeAffinity: description: Describes node affinity scheduling rules for the pod. properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node matches the corresponding matchExpressions; - the node(s) with the highest sum are the most preferred. + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects - (i.e. is also a no-op). + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). properties: preference: description: A node selector term, associated with @@ -1775,32 +1794,26 @@ spec: description: A list of node selector requirements by node's labels. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -1813,32 +1826,26 @@ spec: description: A list of node selector requirements by node's fields. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -1860,53 +1867,46 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from - its node. + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. properties: nodeSelectorTerms: description: Required. A list of node selector terms. The terms are ORed. items: - description: A null or empty node selector term - matches no objects. The requirements of them are - ANDed. The TopologySelectorTerm type implements - a subset of the NodeSelectorTerm. + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. properties: matchExpressions: description: A list of node selector requirements by node's labels. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -1919,32 +1919,26 @@ spec: description: A list of node selector requirements by node's fields. items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -1967,18 +1961,16 @@ spec: other pod(s)). properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. items: description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred @@ -1997,30 +1989,25 @@ spec: of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -2032,53 +2019,45 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -2090,42 +2069,37 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. Empty topologyKey is not allowed. type: string required: - topologyKey type: object weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. format: int32 type: integer required: @@ -2134,23 +2108,22 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to a pod label update), - the system may or may not try to eventually evict the - pod from its node. When there are multiple elements, - the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running properties: labelSelector: description: A label query over a set of resources, @@ -2161,28 +2134,24 @@ spec: selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -2195,51 +2164,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -2252,33 +2214,29 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. type: string required: - topologyKey @@ -2291,18 +2249,16 @@ spec: as some other pod(s)). properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the anti-affinity expressions - specified by this field, but it may choose a node that - violates one or more of the expressions. The node that - is most preferred is the one with the greatest sum of - weights, i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - anti-affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. items: description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred @@ -2321,30 +2277,25 @@ spec: of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -2356,53 +2307,45 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -2414,42 +2357,37 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. Empty topologyKey is not allowed. type: string required: - topologyKey type: object weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. format: int32 type: integer required: @@ -2458,23 +2396,22 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified - by this field are not met at scheduling time, the pod - will not be scheduled onto the node. If the anti-affinity - requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod - label update), the system may or may not try to eventually - evict the pod from its node. When there are multiple - elements, the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running properties: labelSelector: description: A label query over a set of resources, @@ -2485,28 +2422,24 @@ spec: selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -2519,51 +2452,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -2576,33 +2502,29 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. type: string required: - topologyKey @@ -2610,89 +2532,154 @@ spec: type: array type: object type: object - command: - description: Command startup commands - items: - type: string - type: array customConfig: additionalProperties: type: string - description: CustomConfig sentinel configuration options. + description: Config the config for sentinel type: object - dnsPolicy: - description: DNSPolicy is the DNS policy for the Redis pods. - type: string + enableTLS: + description: EnableTLS enable TLS for redis + type: boolean exporter: - description: Exporter prometheus exporter settings. + description: Exporter defines the specification for the sentinel + exporter properties: enabled: - description: Enabled is the flag to enable sentinel exporter type: boolean image: - description: Image exporter image type: string imagePullPolicy: - description: ImagePullPolicy image pull policy. + description: PullPolicy describes a policy for if/when to + pull a container image type: string type: object - hostNetwork: - description: HostNetwork is the host network settings for the - Redis pods. - type: boolean + expose: + description: Expose + properties: + accessPort: + description: AccessPort defines the lb access nodeport + format: int32 + type: integer + annotations: + additionalProperties: + type: string + description: The annnotations of the service which attached + to services + type: object + dataStorageNodePortMap: + additionalProperties: + format: int32 + type: integer + description: |- + NodePortMap defines the map of the nodeport for redis sentinel only + Reversed for 3.14 backward compatibility + type: object + dataStorageNodePortSequence: + description: |- + NodePortMap defines the map of the nodeport for redis nodes + NodePortSequence defines the sequence of the nodeport for redis cluster only + type: string + image: + description: Image defines the image used to expose redis + from annotations + type: string + imagePullPolicy: + description: ImagePullPolicy defines the image pull policy + type: string + ipFamilyPrefer: + description: IPFamily represents the IP Family (IPv4 or IPv6). + This type is used to express the family of an IP expressed + by a type (e.g. service.spec.ipFamilies). + enum: + - IPv4 + - IPv6 + type: string + type: + description: ServiceType defines the type of the all related + service + enum: + - NodePort + - LoadBalancer + - ClusterIP + type: string + type: object + externalTLSSecret: + description: ExternalTLSSecret the external TLS secret to use, + if not provided, the operator will create one + type: string image: - description: Image is the Redis image to run. + description: Image the redis sentinel image type: string imagePullPolicy: - description: ImagePullPolicy is the Image pull policy. + description: ImagePullPolicy the image pull policy type: string imagePullSecrets: - description: ImagePullSecrets is the list of secrets used to pull - the Redis image from a private registry. + description: ImagePullSecrets the image pull secrets items: - description: LocalObjectReference contains enough information - to let you locate the referenced object inside the same namespace. + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. properties: name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string type: object x-kubernetes-map-type: atomic type: array + monitorConfig: + additionalProperties: + type: string + description: |- + MonitorConfig configs for sentinel to monitor this replication, including: + - down-after-milliseconds + - failover-timeout + - parallel-syncs + type: object nodeSelector: additionalProperties: type: string - description: NodeSelector is the node selector for the Redis pods. type: object + passwordSecret: + description: PasswordSecret + type: string podAnnotations: additionalProperties: type: string - description: PodAnnotations is the annotations for the Redis pods. type: object + quorum: + description: |- + Quorum the number of Sentinels that need to agree about the fact the master is not reachable, + in order to really mark the master as failing, and eventually start a failover procedure if possible. + If not specified, the default value is the majority of the Sentinels. + format: int32 + type: integer replicas: - description: Replicas is the number of Redis replicas to run. + description: Replicas the number of sentinel replicas format: int32 minimum: 3 type: integer resources: - description: Resources is the resource requirements for the Redis - container. + description: Resources the resources for sentinel properties: claims: - description: "Claims lists the names of resources, defined - in spec.resourceClaims, that are used by this container. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: Name must match the name of one entry in - pod.spec.resourceClaims of the Pod where this field - is used. It makes that resource available inside a - container. + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. type: string required: - name @@ -2708,8 +2695,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -2718,75 +2706,80 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed - Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object securityContext: - description: SecurityContext is the security context for the Redis - pods. + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. properties: fsGroup: - description: "A special supplemental group that applies to - all containers in a pod. Some volume types allow the Kubelet - to change the ownership of that volume to be owned by the - pod: \n 1. The owning GID will be the FSGroup 2. The setgid - bit is set (new files created in the volume will be owned - by FSGroup) 3. The permission bits are OR'd with rw-rw---- - \n If unset, the Kubelet will not modify the ownership and - permissions of any volume. Note that this field cannot be - set when spec.os.name is windows." + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer fsGroupChangePolicy: - description: 'fsGroupChangePolicy defines behavior of changing - ownership and permission of the volume before being exposed - inside Pod. This field will only apply to volume types which - support fsGroup based ownership(and permissions). It will - have no effect on ephemeral volume types such as: secret, - configmaps and emptydir. Valid values are "OnRootMismatch" - and "Always". If not specified, "Always" is used. Note that - this field cannot be set when spec.os.name is windows.' + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. type: string runAsGroup: - description: The GID to run the entrypoint of the container - process. Uses runtime default if unset. May also be set - in SecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext - takes precedence for that container. Note that this field - cannot be set when spec.os.name is windows. + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer runAsNonRoot: - description: Indicates that the container must run as a non-root - user. If true, the Kubelet will validate the image at runtime - to ensure that it does not run as UID 0 (root) and fail - to start the container if it does. If unset or false, no - such validation will be performed. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: boolean runAsUser: - description: The UID to run the entrypoint of the container - process. Defaults to user specified in image metadata if - unspecified. May also be set in SecurityContext. If set - in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. - Note that this field cannot be set when spec.os.name is - windows. + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer seLinuxOptions: - description: The SELinux context to be applied to all containers. - If unspecified, the container runtime will allocate a random - SELinux context for each container. May also be set in - SecurityContext. If set in both SecurityContext and PodSecurityContext, - the value specified in SecurityContext takes precedence - for that container. Note that this field cannot be set when - spec.os.name is windows. + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. properties: level: description: Level is SELinux level label that applies @@ -2806,46 +2799,47 @@ spec: type: string type: object seccompProfile: - description: The seccomp options to use by the containers - in this pod. Note that this field cannot be set when spec.os.name - is windows. + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. properties: localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. The profile must - be preconfigured on the node to work. Must be a descending - path, relative to the kubelet's configured seccomp profile - location. Must only be set if type is "Localhost". + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. type: string type: - description: "type indicates which kind of seccomp profile - will be applied. Valid options are: \n Localhost - a - profile defined in a file on the node should be used. - RuntimeDefault - the container runtime default profile - should be used. Unconfined - no profile should be applied." + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. type: string required: - type type: object supplementalGroups: - description: A list of groups applied to the first process - run in each container, in addition to the container's primary - GID, the fsGroup (if specified), and group memberships defined - in the container image for the uid of the container process. - If unspecified, no additional groups are added to any container. - Note that group memberships defined in the container image - for the uid of the container process are still effective, - even if they are not included in this list. Note that this - field cannot be set when spec.os.name is windows. + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. items: format: int64 type: integer type: array sysctls: - description: Sysctls hold a list of namespaced sysctls used - for the pod. Pods with unsupported sysctls (by the container - runtime) might fail to launch. Note that this field cannot - be set when spec.os.name is windows. + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. items: description: Sysctl defines a kernel parameter to be set properties: @@ -2861,174 +2855,172 @@ spec: type: object type: array windowsOptions: - description: The Windows specific settings applied to all - containers. If unspecified, the options within a container's - SecurityContext will be used. If set in both SecurityContext - and PodSecurityContext, the value specified in SecurityContext - takes precedence. Note that this field cannot be set when - spec.os.name is linux. + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. properties: gmsaCredentialSpec: - description: GMSACredentialSpec is where the GMSA admission - webhook (https://github.com/kubernetes-sigs/windows-gmsa) - inlines the contents of the GMSA credential spec named - by the GMSACredentialSpecName field. + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. type: string gmsaCredentialSpecName: description: GMSACredentialSpecName is the name of the GMSA credential spec to use. type: string hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. This field is - alpha-level and will only be honored by components that - enable the WindowsHostProcessContainers feature flag. - Setting this field without the feature flag will result - in errors when validating the Pod. All of a Pod's containers - must have the same effective HostProcess value (it is - not allowed to have a mix of HostProcess containers - and non-HostProcess containers). In addition, if HostProcess - is true then HostNetwork must also be set to true. + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. type: boolean runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. Defaults to the user specified - in image metadata if unspecified. May also be set in - PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext - takes precedence. + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: string type: object type: object + sentinelReference: + description: SentinelReference the sentinel reference + properties: + auth: + description: Auth the sentinel auth + properties: + passwordSecret: + description: PasswordSecret the password secret for redis + type: string + tlsSecret: + description: TLSSecret the tls secret + type: string + username: + description: Username the username for redis + type: string + type: object + nodes: + description: Addresses the sentinel addresses + items: + properties: + flags: + description: Flags + type: string + ip: + description: IP the sentinel node ip + type: string + port: + description: Port the sentinel node port + format: int32 + type: integer + type: object + minItems: 3 + type: array + type: object serviceAnnotations: additionalProperties: type: string - description: ServiceAnnotations is the annotations for the Redis - service. type: object tolerations: - description: Tolerations is the list of tolerations for the Redis - pods. items: - description: The pod this Toleration is attached to tolerates - any taint that matches the triple using - the matching operator . + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . properties: effect: - description: Effect indicates the taint effect to match. - Empty means match all taint effects. When specified, allowed - values are NoSchedule, PreferNoSchedule and NoExecute. + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. type: string key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match - all values and all keys. + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. type: string operator: - description: Operator represents a key's relationship to - the value. Valid operators are Exists and Equal. Defaults - to Equal. Exists is equivalent to wildcard for value, - so that a pod can tolerate all taints of a particular - category. + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. type: string tolerationSeconds: - description: TolerationSeconds represents the period of - time the toleration (which must be of effect NoExecute, - otherwise this field is ignored) tolerates the taint. - By default, it is not set, which means tolerate the taint - forever (do not evict). Zero and negative values will - be treated as 0 (evict immediately) by the system. + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. format: int64 type: integer value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. type: string type: object type: array type: object - serviceMonitor: - description: ServiceMonitor service monitor for prometheus + serviceID: + description: ServiceID the service id for activeredis + format: int32 + maximum: 15 + minimum: 0 + type: integer + type: object + status: + description: RedisFailoverStatus + properties: + detailedStatusRef: + description: DetailedStatusRef detailed status resource ref properties: - customMetricRelabelings: - description: CustomMetricRelabelings custom metric relabelings - type: boolean - interval: - description: Interval + apiVersion: + description: API version of the referent. type: string - metricRelabelings: - description: MetricRelabelConfigs metric relabel configs - items: - description: 'RelabelConfig allows dynamic rewriting of the - label set, being applied to samples before ingestion. It defines - ``-section of Prometheus configuration. - More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs' - properties: - action: - default: replace - description: Action to perform based on regex matching. - Default is 'replace' - enum: - - replace - - keep - - drop - - hashmod - - labelmap - - labeldrop - - labelkeep - type: string - modulus: - description: Modulus to take of the hash of the source label - values. - format: int64 - type: integer - regex: - description: Regular expression against which the extracted - value is matched. Default is '(.*)' - type: string - replacement: - description: Replacement value against which a regex replace - is performed if the regular expression matches. Regex - capture groups are available. Default is '$1' - type: string - separator: - description: Separator placed between concatenated source - label values. default is ';'. - type: string - sourceLabels: - description: The source labels select values from existing - labels. Their content is concatenated using the configured - separator and matched against the configured regular expression - for the replace, keep, and drop actions. - items: - description: LabelName is a valid Prometheus label name - which may only contain ASCII letters, numbers, as well - as underscores. - pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$ - type: string - type: array - targetLabel: - description: Label to which the resulting value is written - in a replace action. It is mandatory for replace actions. - Regex capture groups are available. - type: string - type: object - type: array - scrapeTimeout: - description: ScrapeTimeout + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids type: string type: object - type: object - status: - description: RedisFailoverStatus defines the observed state of RedisFailover - properties: + x-kubernetes-map-type: atomic instance: - description: Instance + description: Instance the redis instance replica info properties: redis: - description: RedisStatusInstanceRedis properties: ready: format: int32 @@ -3038,7 +3030,7 @@ spec: type: integer type: object sentinel: - description: RedisStatusInstanceSentinel + description: Sentinel the sentinel instance info properties: clusterIp: type: string @@ -3054,26 +3046,14 @@ spec: type: integer type: object type: object - lastTransitionTime: - description: Last time the condition transitioned from one status - to another. - format: date-time - type: string - lastUpdateTime: - description: The last time this condition was updated. - format: date-time - type: string master: - description: Master master status + description: Master the redis master access info properties: address: - description: Address master access ip:port type: string name: - description: Name master pod name type: string status: - description: Status master service status type: string required: - address @@ -3081,10 +3061,97 @@ spec: - status type: object message: - description: Message + description: Message the status message type: string + monitor: + description: Monitor the monitor status + properties: + name: + description: Name monitor name + type: string + nodes: + description: Nodes the sentinel monitor nodes + items: + properties: + flags: + description: Flags + type: string + ip: + description: IP the sentinel node ip + type: string + port: + description: Port the sentinel node port + format: int32 + type: integer + type: object + type: array + oldPasswordSecret: + description: OldPasswordSecret + type: string + passwordSecret: + description: PasswordSecret + type: string + policy: + description: Policy the failover policy + type: string + tlsSecret: + description: TLSSecret the tls secret + type: string + username: + description: Username sentinel username + type: string + type: object + nodes: + description: Nodes the redis cluster nodes + items: + description: RedisNode represent a RedisCluster Node + properties: + id: + description: ID is the redis cluster node id, not runid + type: string + ip: + description: IP is the ip of the node. if access announce is + enabled, it will be the access ip + type: string + masterRef: + description: MasterRef is the master node id of this node + type: string + nodeName: + description: NodeName is the node name of the node where holds + the pod + type: string + podName: + description: PodName current pod name + type: string + port: + description: Port is the port of the node. if access announce + is enabled, it will be the access port + type: string + role: + description: Role is the role of the node, master or slave + type: string + slots: + description: 'Slots is the slot range for the shard, eg: 0-1000,1002,1005-1100' + items: + type: string + type: array + statefulSet: + description: StatefulSet is the statefulset name of this pod + type: string + required: + - ip + - nodeName + - podName + - port + - role + - statefulSet + type: object + type: array phase: - description: Creating, Pending, Fail, Ready + description: Phase + type: string + tlsSecret: + description: TLSSecret the tls secret type: string version: description: Version diff --git a/config/crd/bases/databases.spotahome.com_redissentinels.yaml b/config/crd/bases/databases.spotahome.com_redissentinels.yaml new file mode 100644 index 0000000..f159a34 --- /dev/null +++ b/config/crd/bases/databases.spotahome.com_redissentinels.yaml @@ -0,0 +1,1268 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: redissentinels.databases.spotahome.com +spec: + group: databases.spotahome.com + names: + kind: RedisSentinel + listKind: RedisSentinelList + plural: redissentinels + singular: redissentinel + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Sentinel replicas + jsonPath: .spec.replicas + name: Replicas + type: integer + - description: Instance access type + jsonPath: .spec.expose.type + name: Access + type: string + - description: Instance phase + jsonPath: .status.phase + name: Status + type: string + - description: Instance status message + jsonPath: .status.message + name: Message + type: string + - description: Time since creation + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: RedisSentinel is the Schema for the redissentinels API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RedisSentinelSpec defines the desired state of RedisSentinel + properties: + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + customConfig: + additionalProperties: + type: string + description: Config the config for sentinel + type: object + enableTLS: + description: EnableTLS enable TLS for redis + type: boolean + exporter: + description: Exporter defines the specification for the sentinel exporter + properties: + enabled: + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + type: object + expose: + description: Expose + properties: + accessPort: + description: AccessPort defines the lb access nodeport + format: int32 + type: integer + annotations: + additionalProperties: + type: string + description: The annnotations of the service which attached to + services + type: object + dataStorageNodePortMap: + additionalProperties: + format: int32 + type: integer + description: |- + NodePortMap defines the map of the nodeport for redis sentinel only + Reversed for 3.14 backward compatibility + type: object + dataStorageNodePortSequence: + description: |- + NodePortMap defines the map of the nodeport for redis nodes + NodePortSequence defines the sequence of the nodeport for redis cluster only + type: string + image: + description: Image defines the image used to expose redis from + annotations + type: string + imagePullPolicy: + description: ImagePullPolicy defines the image pull policy + type: string + ipFamilyPrefer: + description: IPFamily represents the IP Family (IPv4 or IPv6). + This type is used to express the family of an IP expressed by + a type (e.g. service.spec.ipFamilies). + enum: + - IPv4 + - IPv6 + type: string + type: + description: ServiceType defines the type of the all related service + enum: + - NodePort + - LoadBalancer + - ClusterIP + type: string + type: object + externalTLSSecret: + description: ExternalTLSSecret the external TLS secret to use, if + not provided, the operator will create one + type: string + image: + description: Image the redis sentinel image + type: string + imagePullPolicy: + description: ImagePullPolicy the image pull policy + type: string + imagePullSecrets: + description: ImagePullSecrets the image pull secrets + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + nodeSelector: + additionalProperties: + type: string + type: object + passwordSecret: + description: PasswordSecret + type: string + podAnnotations: + additionalProperties: + type: string + type: object + replicas: + description: Replicas the number of sentinel replicas + format: int32 + minimum: 3 + type: integer + resources: + description: Resources the resources for sentinel + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAnnotations: + additionalProperties: + type: string + type: object + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + type: object + status: + description: RedisSentinelStatus defines the observed state of RedisSentinel + properties: + message: + description: Message the status message + type: string + nodes: + description: Nodes the redis cluster nodes + items: + description: RedisNode represent a RedisCluster Node + properties: + id: + description: ID is the redis cluster node id, not runid + type: string + ip: + description: IP is the ip of the node. if access announce is + enabled, it will be the access ip + type: string + masterRef: + description: MasterRef is the master node id of this node + type: string + nodeName: + description: NodeName is the node name of the node where holds + the pod + type: string + podName: + description: PodName current pod name + type: string + port: + description: Port is the port of the node. if access announce + is enabled, it will be the access port + type: string + role: + description: Role is the role of the node, master or slave + type: string + slots: + description: 'Slots is the slot range for the shard, eg: 0-1000,1002,1005-1100' + items: + type: string + type: array + statefulSet: + description: StatefulSet is the statefulset name of this pod + type: string + required: + - ip + - nodeName + - podName + - port + - role + - statefulSet + type: object + type: array + phase: + description: Phase the status phase + type: string + tlsSecret: + description: TLSSecret the tls secret + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/middleware.alauda.io_imageversions.yaml b/config/crd/bases/middleware.alauda.io_imageversions.yaml new file mode 100644 index 0000000..ae2da84 --- /dev/null +++ b/config/crd/bases/middleware.alauda.io_imageversions.yaml @@ -0,0 +1,84 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: imageversions.middleware.alauda.io +spec: + group: middleware.alauda.io + names: + kind: ImageVersion + listKind: ImageVersionList + plural: imageversions + singular: imageversion + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ImageVersion is the Schema for the imageversions API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ImageVersionSpec defines the desired state of ImageVersion + properties: + components: + additionalProperties: + properties: + coreComponent: + type: boolean + versions: + items: + properties: + displayVersion: + type: string + extensions: + additionalProperties: + properties: + version: + type: string + required: + - version + type: object + type: object + image: + type: string + tag: + type: string + version: + type: string + required: + - image + - tag + type: object + type: array + required: + - versions + type: object + type: object + crVersion: + description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file' + type: string + type: object + status: + description: ImageVersionStatus defines the observed state of ImageVersion + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/middleware.alauda.io_redis.yaml b/config/crd/bases/middleware.alauda.io_redis.yaml new file mode 100644 index 0000000..c66a97b --- /dev/null +++ b/config/crd/bases/middleware.alauda.io_redis.yaml @@ -0,0 +1,4078 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: redis.middleware.alauda.io +spec: + group: middleware.alauda.io + names: + kind: Redis + listKind: RedisList + plural: redis + singular: redis + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Instance arch + jsonPath: .spec.arch + name: Arch + type: string + - description: Redis version + jsonPath: .spec.version + name: Version + type: string + - description: Instance access type + jsonPath: .spec.expose.type + name: Access + type: string + - description: Instance phase + jsonPath: .status.phase + name: Status + type: string + - description: Instance status message + jsonPath: .status.message + name: Message + type: string + - description: Bundle Version + jsonPath: .status.upgradeStatus.crVersion + name: Bundle Version + type: string + - description: Enable instance auto upgrade + jsonPath: .spec.upgradeOption.autoUpgrade + name: AutoUpgrade + type: boolean + - description: Time since creation + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: Redis is the Schema for the redis API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RedisSpec defines the desired state of Redis + properties: + affinity: + description: Affinity specifies the affinity for the Pod + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + affinityPolicy: + description: AffinityPolicy support SoftAntiAffinity, AntiAffinityInSharding, + AntiAffinity, Default SoftAntiAffinity + enum: + - SoftAntiAffinity + - AntiAffinityInSharding + - AntiAffinity + type: string + arch: + description: Arch supports cluster, sentinel + enum: + - cluster + - sentinel + - standalone + type: string + backup: + description: Backup holds information for Redis backups + properties: + image: + type: string + schedule: + items: + properties: + keep: + format: int32 + type: integer + keepAfterDeletion: + type: boolean + name: + type: string + schedule: + type: string + storage: + properties: + size: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + storageClassName: + type: string + type: object + target: + properties: + s3Option: + description: S3Option + properties: + bucket: + type: string + dir: + type: string + s3Secret: + type: string + type: object + type: object + required: + - keep + - schedule + - storage + type: object + type: array + type: object + customConfig: + additionalProperties: + type: string + description: CustomConfig defines custom Redis configuration settings. + Some of these settings can be modified using the config set command + at runtime. + type: object + enableActiveRedis: + description: EnableActiveRedis enable active-active model for Redis + type: boolean + enableTLS: + description: EnableTLS enables TLS for Redis + type: boolean + exporter: + description: Exporter defines Redis exporter settings + properties: + enabled: + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + expose: + description: ' Expose defines information for Redis nodePorts settings' + properties: + accessPort: + description: AccessPort defines the lb access nodeport + format: int32 + type: integer + annotations: + additionalProperties: + type: string + description: The annnotations of the service which attached to + services + type: object + dataStorageNodePortMap: + additionalProperties: + format: int32 + type: integer + description: |- + NodePortMap defines the map of the nodeport for redis sentinel only + Reversed for 3.14 backward compatibility + type: object + dataStorageNodePortSequence: + description: |- + NodePortMap defines the map of the nodeport for redis nodes + NodePortSequence defines the sequence of the nodeport for redis cluster only + type: string + enableNodePort: + description: EnableNodePort defines if the nodeport is enabled + type: boolean + type: + description: ServiceType defines the type of the all related service + enum: + - NodePort + - LoadBalancer + - ClusterIP + type: string + type: object + ipFamilyPrefer: + description: |- + IPFamilyPrefer sets the preferable IP family for the Pod and Redis + IPFamily represents the IP Family (IPv4 or IPv6). This type is used to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). + enum: + - IPv4 + - IPv6 + - "" + type: string + nodeSelector: + additionalProperties: + type: string + description: NodeSelector specifies the node selector for the Pod + type: object + passwordSecret: + description: PasswordSecret set the Kubernetes Secret containing the + Redis password PasswordSecret string,key `password` + type: string + patches: + description: Provides the ability to patch the generated manifest + of several child resources. + properties: + services: + description: Patch configuration for the Service created to serve + traffic to the cluster. + items: + description: |- + Patch configuration for the Service created to serve traffic to the cluster. + Allows for the manifest of the created Service to be overwritten with custom configuration. + properties: + metadata: + description: |- + EmbeddedObjectMeta is an embedded subset of the fields included in k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta. + Only fields which are relevant to embedded resources are included. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + Map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + name: + description: |- + Name must be unique within a namespace. Is required when creating resources, although + some resources may allow a client to request the generation of an appropriate name + automatically. Name is primarily intended for creation idempotence and configuration + definition. + Cannot be updated. + More info: http://kubernetes.io/docs/user-guide/identifiers#names + type: string + namespace: + description: |- + Namespace defines the space within each name must be unique. An empty namespace is + equivalent to the "default" namespace, but "default" is the canonical representation. + Not all objects are required to be scoped to a namespace - the value of this field for + those objects will be empty. + + Must be a DNS_LABEL. + Cannot be updated. + More info: http://kubernetes.io/docs/user-guide/namespaces + type: string + type: object + spec: + description: |- + Spec defines the behavior of a Service. + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + allocateLoadBalancerNodePorts: + description: |- + allocateLoadBalancerNodePorts defines if NodePorts will be automatically + allocated for services with type LoadBalancer. Default is "true". It + may be set to "false" if the cluster load-balancer does not rely on + NodePorts. If the caller requests specific NodePorts (by specifying a + value), those requests will be respected, regardless of this field. + This field may only be set for services with type LoadBalancer and will + be cleared if the type is changed to any other type. + type: boolean + clusterIP: + description: |- + clusterIP is the IP address of the service and is usually assigned + randomly. If an address is specified manually, is in-range (as per + system configuration), and is not in use, it will be allocated to the + service; otherwise creation of the service will fail. This field may not + be changed through updates unless the type field is also being changed + to ExternalName (which requires this field to be blank) or the type + field is being changed from ExternalName (in which case this field may + optionally be specified, as describe above). Valid values are "None", + empty string (""), or a valid IP address. Setting this to "None" makes a + "headless service" (no virtual IP), which is useful when direct endpoint + connections are preferred and proxying is not required. Only applies to + types ClusterIP, NodePort, and LoadBalancer. If this field is specified + when creating a Service of type ExternalName, creation will fail. This + field will be wiped when updating a Service to type ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + type: string + clusterIPs: + description: |- + ClusterIPs is a list of IP addresses assigned to this service, and are + usually assigned randomly. If an address is specified manually, is + in-range (as per system configuration), and is not in use, it will be + allocated to the service; otherwise creation of the service will fail. + This field may not be changed through updates unless the type field is + also being changed to ExternalName (which requires this field to be + empty) or the type field is being changed from ExternalName (in which + case this field may optionally be specified, as describe above). Valid + values are "None", empty string (""), or a valid IP address. Setting + this to "None" makes a "headless service" (no virtual IP), which is + useful when direct endpoint connections are preferred and proxying is + not required. Only applies to types ClusterIP, NodePort, and + LoadBalancer. If this field is specified when creating a Service of type + ExternalName, creation will fail. This field will be wiped when updating + a Service to type ExternalName. If this field is not specified, it will + be initialized from the clusterIP field. If this field is specified, + clients must ensure that clusterIPs[0] and clusterIP have the same + value. + + This field may hold a maximum of two entries (dual-stack IPs, in either order). + These IPs must correspond to the values of the ipFamilies field. Both + clusterIPs and ipFamilies are governed by the ipFamilyPolicy field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + description: |- + externalIPs is a list of IP addresses for which nodes in the cluster + will also accept traffic for this service. These IPs are not managed by + Kubernetes. The user is responsible for ensuring that traffic arrives + at a node with this IP. A common example is external load-balancers + that are not part of the Kubernetes system. + items: + type: string + type: array + externalName: + description: |- + externalName is the external reference that discovery mechanisms will + return as an alias for this service (e.g. a DNS CNAME record). No + proxying will be involved. Must be a lowercase RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires `type` to be "ExternalName". + type: string + externalTrafficPolicy: + description: |- + externalTrafficPolicy describes how nodes distribute service traffic they + receive on one of the Service's "externally-facing" addresses (NodePorts, + ExternalIPs, and LoadBalancer IPs). If set to "Local", the proxy will configure + the service in a way that assumes that external load balancers will take care + of balancing the service traffic between nodes, and so each node will deliver + traffic only to the node-local endpoints of the service, without masquerading + the client source IP. (Traffic mistakenly sent to a node with no endpoints will + be dropped.) The default value, "Cluster", uses the standard behavior of + routing to all endpoints evenly (possibly modified by topology and other + features). Note that traffic sent to an External IP or LoadBalancer IP from + within the cluster will always get "Cluster" semantics, but clients sending to + a NodePort from within the cluster may need to take traffic policy into account + when picking a node. + type: string + healthCheckNodePort: + description: |- + healthCheckNodePort specifies the healthcheck nodePort for the service. + This only applies when type is set to LoadBalancer and + externalTrafficPolicy is set to Local. If a value is specified, is + in-range, and is not in use, it will be used. If not specified, a value + will be automatically allocated. External systems (e.g. load-balancers) + can use this port to determine if a given node holds endpoints for this + service or not. If this field is specified when creating a Service + which does not need it, creation will fail. This field will be wiped + when updating a Service to no longer need it (e.g. changing type). + This field cannot be updated once set. + format: int32 + type: integer + internalTrafficPolicy: + description: |- + InternalTrafficPolicy describes how nodes distribute service traffic they + receive on the ClusterIP. If set to "Local", the proxy will assume that pods + only want to talk to endpoints of the service on the same node as the pod, + dropping the traffic if there are no local endpoints. The default value, + "Cluster", uses the standard behavior of routing to all endpoints evenly + (possibly modified by topology and other features). + type: string + ipFamilies: + description: |- + IPFamilies is a list of IP families (e.g. IPv4, IPv6) assigned to this + service. This field is usually assigned automatically based on cluster + configuration and the ipFamilyPolicy field. If this field is specified + manually, the requested family is available in the cluster, + and ipFamilyPolicy allows it, it will be used; otherwise creation of + the service will fail. This field is conditionally mutable: it allows + for adding or removing a secondary IP family, but it does not allow + changing the primary IP family of the Service. Valid values are "IPv4" + and "IPv6". This field only applies to Services of types ClusterIP, + NodePort, and LoadBalancer, and does apply to "headless" services. + This field will be wiped when updating a Service to type ExternalName. + + This field may hold a maximum of two entries (dual-stack families, in + either order). These families must correspond to the values of the + clusterIPs field, if specified. Both clusterIPs and ipFamilies are + governed by the ipFamilyPolicy field. + items: + description: |- + IPFamily represents the IP Family (IPv4 or IPv6). This type is used + to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + description: |- + IPFamilyPolicy represents the dual-stack-ness requested or required by + this Service. If there is no value provided, then this field will be set + to SingleStack. Services can be "SingleStack" (a single IP family), + "PreferDualStack" (two IP families on dual-stack configured clusters or + a single IP family on single-stack clusters), or "RequireDualStack" + (two IP families on dual-stack configured clusters, otherwise fail). The + ipFamilies and clusterIPs fields depend on the value of this field. This + field will be wiped when updating a service to type ExternalName. + type: string + loadBalancerClass: + description: |- + loadBalancerClass is the class of the load balancer implementation this Service belongs to. + If specified, the value of this field must be a label-style identifier, with an optional prefix, + e.g. "internal-vip" or "example.com/internal-vip". Unprefixed names are reserved for end-users. + This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load + balancer implementation is used, today this is typically done through the cloud provider integration, + but should apply for any default implementation. If set, it is assumed that a load balancer + implementation is watching for Services with a matching class. Any default load balancer + implementation (e.g. cloud providers) should ignore Services that set this field. + This field can only be set when creating or updating a Service to type 'LoadBalancer'. + Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type. + type: string + loadBalancerIP: + description: |- + Only applies to Service Type: LoadBalancer. + This feature depends on whether the underlying cloud-provider supports specifying + the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not support the feature. + Deprecated: This field was under-specified and its meaning varies across implementations. + Using it is non-portable and it may not support dual-stack. + Users are encouraged to use implementation-specific annotations when available. + type: string + loadBalancerSourceRanges: + description: |- + If specified and supported by the platform, this will restrict traffic through the cloud-provider + load-balancer will be restricted to the specified client IPs. This field will be ignored if the + cloud-provider does not support the feature." + More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/ + items: + type: string + type: array + ports: + description: |- + The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + items: + description: ServicePort contains information on service's + port. + properties: + appProtocol: + description: |- + The application protocol for this port. + This is used as a hint for implementations to offer richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: + + * Un-prefixed protocol names - reserved for IANA standard service names (as per + RFC-6335 and https://www.iana.org/assignments/service-names). + + * Kubernetes-defined prefixed names: + * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 + * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 + + * Other protocols should use implementation-defined prefixed names such as + mycompany.com/my-custom-protocol. + type: string + name: + description: |- + The name of this port within the service. This must be a DNS_LABEL. + All ports within a ServiceSpec must have unique names. When considering + the endpoints for a Service, this must match the 'name' field in the + EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: |- + The port on each node on which this service is exposed when type is + NodePort or LoadBalancer. Usually assigned by the system. If a value is + specified, in-range, and not in use it will be used, otherwise the + operation will fail. If not specified, a port will be allocated if this + Service requires one. If this field is specified when creating a + Service which does not need it, creation will fail. This field will be + wiped when updating a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). + More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + format: int32 + type: integer + port: + description: The port that will be exposed by + this service. + format: int32 + type: integer + protocol: + allOf: + - default: TCP + - default: TCP + description: |- + The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". + Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the pods targeted by the service. + Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named port in the + target Pod's container ports. If this is not specified, the value + of the 'port' field is used (an identity map). + This field is ignored for services with clusterIP=None, and should be + omitted or set equal to the 'port' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + description: |- + publishNotReadyAddresses indicates that any agent which deals with endpoints for this + Service should disregard any indications of ready/not-ready. + The primary use case for setting this field is for a StatefulSet's Headless Service to + propagate SRV DNS records for its Pods for the purpose of peer discovery. + The Kubernetes controllers that generate Endpoints and EndpointSlice resources for + Services interpret this to mean that all endpoints are considered "ready" even if the + Pods themselves are not. Agents which consume only Kubernetes generated endpoints + through the Endpoints or EndpointSlice resources can safely assume this behavior. + type: boolean + selector: + additionalProperties: + type: string + description: |- + Route service traffic to pods with label keys and values matching this + selector. If empty or not present, the service is assumed to have an + external process managing its endpoints, which Kubernetes will not + modify. Only applies to types ClusterIP, NodePort, and LoadBalancer. + Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + description: |- + Supports "ClientIP" and "None". Used to maintain session affinity. + Enable client IP based session affinity. + Must be ClientIP or None. + Defaults to None. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations + of Client IP based session affinity. + properties: + timeoutSeconds: + description: |- + timeoutSeconds specifies the seconds of ClientIP type session sticky time. + The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP". + Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: |- + type determines how the Service is exposed. Defaults to ClusterIP. Valid + options are ExternalName, ClusterIP, NodePort, and LoadBalancer. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or if that is not + specified, by manual construction of an Endpoints object or + EndpointSlice objects. If clusterIP is "None", no virtual IP is + allocated and the endpoints are published as a set of endpoints rather + than a virtual IP. + "NodePort" builds on ClusterIP and allocates a port on every node which + routes to the same endpoints as the clusterIP. + "LoadBalancer" builds on NodePort and creates an external load-balancer + (if supported in the current cloud) which routes to the same endpoints + as the clusterIP. + "ExternalName" aliases this service to the specified externalName. + Several other fields do not apply to ExternalName services. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: string + type: object + type: object + type: array + type: object + pause: + description: Pause field indicates whether Redis is paused. + type: boolean + persistent: + description: Persistent for Redis + properties: + storageClassName: + description: This field specifies the name of the storage class + that should be used for the persistent storage of Redis + type: string + required: + - storageClassName + type: object + persistentSize: + anyOf: + - type: integer + - type: string + description: PersistentSize set the size of the persistent volume + for the Redis + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations holds Kubernetes Pod annotations PodAnnotations + type: object + redisProxy: + description: RedisProxy defines RedisProxy settings + properties: + affinity: + description: If specified, affinity will define the pod's scheduling + constraints + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + config: + additionalProperties: + type: string + description: a map holding additional configuration options for + the proxy. + type: object + enable: + description: a boolean indicating whether or not the Redis Proxy + service is enabled. + type: boolean + image: + description: a string representing the Docker image to use for + the proxy. + type: string + nodeSelector: + additionalProperties: + type: string + description: |- + NodeSelector is a selector which must be true for the pod to fit on a node. + Selector which must match a node's labels for the pod to be scheduled on that node. + More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + type: object + replicas: + description: an integer indicating the number of replicas to create + for the proxy. + format: int32 + type: integer + resources: + description: Resources holds ResourceRequirements for the MySQL + Agent & Server Containers + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + tolerations: + description: |- + Tolerations allows specifying a list of tolerations for controlling which + set of Nodes a Pod can be scheduled on + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + type: object + replicas: + description: Replicas defines desired number of replicas for Redis + properties: + cluster: + description: This field specifies the number of replicas for Redis + Cluster + properties: + shard: + description: This field specifies the number of master in + Redis Cluster. + format: int32 + minimum: 3 + type: integer + shards: + description: |- + This field specifies the assignment of cluster shard slots. + this config is only works for new create instance, update will not take effect after instance is startup + items: + properties: + slots: + description: 'Slots is the slot range for the shard, + eg: 0-1000,1002,1005-1100' + pattern: ^(\d{1,5}|(\d{1,5}-\d{1,5}))(,(\d{1,5}|(\d{1,5}-\d{1,5})))*$ + type: string + type: object + type: array + slave: + description: This field specifies the number of replica nodes + per Redis Cluster master. + format: int32 + maximum: 5 + minimum: 0 + type: integer + required: + - shard + type: object + sentinel: + description: This field specifies the number of replicas for Redis + sentinel + properties: + master: + description: sentinel master nodes, only 1 + format: int32 + maximum: 1 + minimum: 1 + type: integer + slave: + description: This field specifies the number of replica nodes. + format: int32 + maximum: 5 + minimum: 0 + type: integer + required: + - master + type: object + type: object + resources: + description: Resources for setting resource requirements for the Pod + Resources *v1.ResourceRequirements + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restore: + description: Restore contains information for Redis + properties: + backupName: + type: string + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + type: object + securityContext: + description: SecurityContext sets security attributes for the Pod + SecurityContex + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + sentinel: + description: Sentinel defines Sentinel configuration settings Sentinel + properties: + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + customConfig: + additionalProperties: + type: string + description: Config the config for sentinel + type: object + enableTLS: + description: EnableTLS enable TLS for redis + type: boolean + exporter: + description: Exporter defines the specification for the sentinel + exporter + properties: + enabled: + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + type: object + expose: + description: Expose + properties: + accessPort: + description: AccessPort defines the lb access nodeport + format: int32 + type: integer + annotations: + additionalProperties: + type: string + description: The annnotations of the service which attached + to services + type: object + dataStorageNodePortMap: + additionalProperties: + format: int32 + type: integer + description: |- + NodePortMap defines the map of the nodeport for redis sentinel only + Reversed for 3.14 backward compatibility + type: object + dataStorageNodePortSequence: + description: |- + NodePortMap defines the map of the nodeport for redis nodes + NodePortSequence defines the sequence of the nodeport for redis cluster only + type: string + image: + description: Image defines the image used to expose redis + from annotations + type: string + imagePullPolicy: + description: ImagePullPolicy defines the image pull policy + type: string + ipFamilyPrefer: + description: IPFamily represents the IP Family (IPv4 or IPv6). + This type is used to express the family of an IP expressed + by a type (e.g. service.spec.ipFamilies). + enum: + - IPv4 + - IPv6 + type: string + type: + description: ServiceType defines the type of the all related + service + enum: + - NodePort + - LoadBalancer + - ClusterIP + type: string + type: object + externalTLSSecret: + description: ExternalTLSSecret the external TLS secret to use, + if not provided, the operator will create one + type: string + image: + description: Image the redis sentinel image + type: string + imagePullPolicy: + description: ImagePullPolicy the image pull policy + type: string + imagePullSecrets: + description: ImagePullSecrets the image pull secrets + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + monitorConfig: + additionalProperties: + type: string + description: |- + MonitorConfig configs for sentinel to monitor this replication, including: + - down-after-milliseconds + - failover-timeout + - parallel-syncs + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passwordSecret: + description: PasswordSecret + type: string + podAnnotations: + additionalProperties: + type: string + type: object + quorum: + description: |- + Quorum the number of Sentinels that need to agree about the fact the master is not reachable, + in order to really mark the master as failing, and eventually start a failover procedure if possible. + If not specified, the default value is the majority of the Sentinels. + format: int32 + type: integer + replicas: + description: Replicas the number of sentinel replicas + format: int32 + minimum: 3 + type: integer + resources: + description: Resources the resources for sentinel + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + sentinelReference: + description: SentinelReference the sentinel reference + properties: + auth: + description: Auth the sentinel auth + properties: + passwordSecret: + description: PasswordSecret the password secret for redis + type: string + tlsSecret: + description: TLSSecret the tls secret + type: string + username: + description: Username the username for redis + type: string + type: object + nodes: + description: Addresses the sentinel addresses + items: + properties: + flags: + description: Flags + type: string + ip: + description: IP the sentinel node ip + type: string + port: + description: Port the sentinel node port + format: int32 + type: integer + type: object + minItems: 3 + type: array + type: object + serviceAnnotations: + additionalProperties: + type: string + type: object + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + type: object + sentinelCustomConfig: + additionalProperties: + type: string + description: SentinelCustomConfig defines custom Sentinel configuration + settings + type: object + serviceID: + description: ServiceID the service id for activeredis + format: int32 + maximum: 15 + minimum: 0 + type: integer + tolerations: + description: tolerations defines tolerations for the Pod + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + upgradeOption: + description: UpgradeOption defines the upgrade strategy for the Redis + instance. + properties: + autoUpgrade: + description: AutoUpgrade whether upgrade automatically + type: boolean + crVersion: + description: CRVersion indicates the version to upgrade to. + type: string + type: object + version: + description: Version supports 5.0, 6.0, 6.2, 7.0, 7.2, 7.4 + enum: + - "5.0" + - "6.0" + - "6.2" + - "7.0" + - "7.2" + - "7.4" + type: string + required: + - arch + - resources + - version + type: object + status: + description: RedisStatus defines the observed state of Redis + properties: + clusterNodes: + description: ClusterNodes redis nodes info + items: + description: RedisNode represent a RedisCluster Node + properties: + id: + description: ID is the redis cluster node id, not runid + type: string + ip: + description: IP is the ip of the node. if access announce is + enabled, it will be the access ip + type: string + masterRef: + description: MasterRef is the master node id of this node + type: string + nodeName: + description: NodeName is the node name of the node where holds + the pod + type: string + podName: + description: PodName current pod name + type: string + port: + description: Port is the port of the node. if access announce + is enabled, it will be the access port + type: string + role: + description: Role is the role of the node, master or slave + type: string + slots: + description: 'Slots is the slot range for the shard, eg: 0-1000,1002,1005-1100' + items: + type: string + type: array + statefulSet: + description: StatefulSet is the statefulset name of this pod + type: string + required: + - ip + - nodeName + - podName + - port + - role + - statefulSet + type: object + type: array + detailedStatusRef: + description: DetailedStatusRef detailed status resource ref + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + lastShardCount: + description: LastShardCount indicates the last number of shards in + the Redis Cluster. + format: int32 + type: integer + lastVersion: + description: LastVersion indicates the last version of the Redis instance. + type: string + matchLabels: + additionalProperties: + type: string + description: Matching labels selector for Redis + type: object + message: + description: This field contains an additional message for the instance's + status + type: string + passwordSecretName: + description: The name of the kubernetes Secret that contains Redis + password. + type: string + phase: + description: |- + Phase indicates whether all the resource for the instance is ok. + Values are as below: + Initializing - Resource is in Initializing or Reconcile + Ready - All resources is ok. In most cases, Ready means the cluster is ok to use + Error - Error found when do resource init + type: string + proxyMatchLabels: + additionalProperties: + type: string + description: Matching label selector for Redis proxy. + type: object + proxyServiceName: + description: The name of the kubernetes Service for Redis Proxy + type: string + restored: + description: |- + Restored indicates whether the instance has been restored from a backup. + if the instance is set to restore from a backup, when the restore is completed, the restored field will be set to true. + type: boolean + serviceName: + description: The name of the kubernetes Service for Redis + type: string + upgradeStatus: + description: UpgradeStatus indicates the status of the bundle upgrade. + properties: + crVersion: + description: CRVersion indicates the version to upgrade to. + type: string + message: + description: Message indicates the message of the upgrade. + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/redis.kun_distributedredisclusters.yaml b/config/crd/bases/redis.kun_distributedredisclusters.yaml index b284782..31f84bb 100644 --- a/config/crd/bases/redis.kun_distributedredisclusters.yaml +++ b/config/crd/bases/redis.kun_distributedredisclusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.16.0 name: distributedredisclusters.redis.kun spec: group: redis.kun @@ -23,6 +23,10 @@ spec: jsonPath: .status.clusterStatus name: Service Status type: string + - description: Instance access type + jsonPath: .spec.expose.type + name: Access + type: string - description: Instance status jsonPath: .status.status name: Status @@ -31,6 +35,10 @@ spec: jsonPath: .status.reason name: Message type: string + - description: Time since creation + jsonPath: .metadata.creationTimestamp + name: Age + type: date name: v1alpha1 schema: openAPIV3Schema: @@ -38,14 +46,19 @@ spec: API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -61,22 +74,20 @@ spec: pod. properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the affinity expressions specified by - this field, but it may choose a node that violates one or - more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node matches - the corresponding matchExpressions; the node(s) with the - highest sum are the most preferred. + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects (i.e. - is also a no-op). + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). properties: preference: description: A node selector term, associated with the @@ -86,30 +97,26 @@ spec: description: A list of node selector requirements by node's labels. items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -122,30 +129,26 @@ spec: description: A list of node selector requirements by node's fields. items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -167,50 +170,46 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this - field are not met at scheduling time, the pod will not be - scheduled onto the node. If the affinity requirements specified - by this field cease to be met at some point during pod execution - (e.g. due to an update), the system may or may not try to - eventually evict the pod from its node. + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. properties: nodeSelectorTerms: description: Required. A list of node selector terms. The terms are ORed. items: - description: A null or empty node selector term matches - no objects. The requirements of them are ANDed. The - TopologySelectorTerm type implements a subset of the - NodeSelectorTerm. + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. properties: matchExpressions: description: A list of node selector requirements by node's labels. items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -223,30 +222,26 @@ spec: description: A list of node selector requirements by node's fields. items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: key: description: The label key that the selector applies to. type: string operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. type: string values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. items: type: string type: array @@ -268,16 +263,15 @@ spec: this pod in the same node, zone, etc. as some other pod(s)). properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the affinity expressions specified by - this field, but it may choose a node that violates one or - more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node has - pods which matches the corresponding podAffinityTerm; the + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. items: description: The weights of all of the matched WeightedPodAffinityTerm @@ -296,28 +290,24 @@ spec: selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -330,51 +320,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -387,40 +370,37 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. type: string required: - topologyKey type: object weight: - description: weight associated with matching the corresponding - podAffinityTerm, in the range 1-100. + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. format: int32 type: integer required: @@ -429,23 +409,22 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this - field are not met at scheduling time, the pod will not be - scheduled onto the node. If the affinity requirements specified - by this field cease to be met at some point during pod execution - (e.g. due to a pod label update), the system may or may - not try to eventually evict the pod from its node. When - there are multiple elements, the lists of nodes corresponding - to each podAffinityTerm are intersected, i.e. all terms - must be satisfied. + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not co-located - (anti-affinity) with, where co-located is defined as running - on a node whose value of the label with key - matches that of any node on which a pod of the set of - pods is running + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running properties: labelSelector: description: A label query over a set of resources, @@ -455,26 +434,25 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -486,47 +464,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied to the - union of the namespaces selected by this field and - the ones listed in the namespaces field. null selector - and null or empty namespaces list means "this pod's - namespace". An empty selector ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -538,32 +513,28 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list of namespace - names that the term applies to. The term is applied - to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. null or - empty namespaces list and null namespaceSelector means - "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where - co-located is defined as running on a node whose value - of the label with key topologyKey matches that of - any node on which any of the selected pods is running. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. Empty topologyKey is not allowed. type: string required: @@ -577,16 +548,15 @@ spec: other pod(s)). properties: preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the anti-affinity expressions specified - by this field, but it may choose a node that violates one - or more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling anti-affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node has - pods which matches the corresponding podAffinityTerm; the + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. items: description: The weights of all of the matched WeightedPodAffinityTerm @@ -605,28 +575,24 @@ spec: selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -639,51 +605,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. items: type: string @@ -696,40 +655,37 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. type: string required: - topologyKey type: object weight: - description: weight associated with matching the corresponding - podAffinityTerm, in the range 1-100. + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. format: int32 type: integer required: @@ -738,23 +694,22 @@ spec: type: object type: array requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the anti-affinity requirements - specified by this field cease to be met at some point during - pod execution (e.g. due to a pod label update), the system - may or may not try to eventually evict the pod from its - node. When there are multiple elements, the lists of nodes - corresponding to each podAffinityTerm are intersected, i.e. - all terms must be satisfied. + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not co-located - (anti-affinity) with, where co-located is defined as running - on a node whose value of the label with key - matches that of any node on which a pod of the set of - pods is running + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running properties: labelSelector: description: A label query over a set of resources, @@ -764,26 +719,25 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -795,47 +749,44 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied to the - union of the namespaces selected by this field and - the ones listed in the namespaces field. null selector - and null or empty namespaces list means "this pod's - namespace". An empty selector ({}) matches all namespaces. + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. items: type: string type: array @@ -847,32 +798,28 @@ spec: matchLabels: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic namespaces: - description: namespaces specifies a static list of namespace - names that the term applies to. The term is applied - to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. null or - empty namespaces list and null namespaceSelector means - "this pod's namespace". + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". items: type: string type: array topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where - co-located is defined as running on a node whose value - of the label with key topologyKey matches that of - any node on which any of the selected pods is running. + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. Empty topologyKey is not allowed. type: string required: @@ -891,63 +838,45 @@ spec: annotations: additionalProperties: type: string - description: Annotations annotations inject to redis pods type: object backup: description: Set backup schedule properties: image: - description: Image is the Redis backup image to run. type: string schedule: - description: Schedule is the backup schedule. items: - description: Schedule properties: keep: - description: Keep is the number of backups to keep. format: int32 - minimum: 1 type: integer keepAfterDeletion: - description: KeepAfterDeletion is the flag to keep the data - after the RedisFailover is deleted. type: boolean name: - description: Name is the scheduled backup name. type: string schedule: - description: Schedule crontab like schedule. type: string storage: - description: Storage is the backup storage configuration. properties: size: anyOf: - type: integer - type: string - description: Size is the size of the PersistentVolumeClaim. pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true storageClassName: - description: StorageClassName is the name of the StorageClass - to use for the PersistentVolumeClaim. type: string type: object target: - description: Target is the backup target configuration. properties: s3Option: - description: S3Option is the S3 backup target configuration. + description: S3Option properties: bucket: - description: Bucket s3 storage bucket type: string dir: - description: Dir s3 storage dir type: string s3Secret: - description: S3Secret s3 storage access secret type: string type: object type: object @@ -959,37 +888,45 @@ spec: type: array type: object clusterReplicas: - description: ClusterReplicas is the number of replicas for each shard + description: ClusterReplicas is the number of replicas for each master + node format: int32 + maximum: 5 + minimum: 0 type: integer command: - description: Command is the Redis image command. items: type: string type: array config: additionalProperties: type: string - description: "Use this map to setup redis service. Most of the settings - is key-value format. \n For client-output-buffer-limit and rename, - the values is split by group." + description: |- + Use this map to setup redis service. Most of the settings is key-value format. + + For client-output-buffer-limit and rename, the values is split by group. type: object containerSecurityContext: - description: ContainerSecurityContext for redis container + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. properties: allowPrivilegeEscalation: - description: 'AllowPrivilegeEscalation controls whether a process - can gain more privileges than its parent process. This bool - directly controls if the no_new_privs flag will be set on the - container process. AllowPrivilegeEscalation is true always when - the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN - Note that this field cannot be set when spec.os.name is windows.' + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. type: boolean capabilities: - description: The capabilities to add/drop when running containers. - Defaults to the default set of capabilities granted by the container - runtime. Note that this field cannot be set when spec.os.name - is windows. + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. properties: add: description: Added capabilities @@ -1005,56 +942,60 @@ spec: type: array type: object privileged: - description: Run container in privileged mode. Processes in privileged - containers are essentially equivalent to root on the host. Defaults - to false. Note that this field cannot be set when spec.os.name - is windows. + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. type: boolean procMount: - description: procMount denotes the type of proc mount to use for - the containers. The default is DefaultProcMount which uses the - container runtime defaults for readonly paths and masked paths. + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: - description: Whether this container has a read-only root filesystem. - Default is false. Note that this field cannot be set when spec.os.name - is windows. + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. type: boolean runAsGroup: - description: The GID to run the entrypoint of the container process. - Uses runtime default if unset. May also be set in PodSecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence. Note that this - field cannot be set when spec.os.name is windows. + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer runAsNonRoot: - description: Indicates that the container must run as a non-root - user. If true, the Kubelet will validate the image at runtime - to ensure that it does not run as UID 0 (root) and fail to start - the container if it does. If unset or false, no such validation - will be performed. May also be set in PodSecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence. + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: boolean runAsUser: - description: The UID to run the entrypoint of the container process. + description: |- + The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. - May also be set in PodSecurityContext. If set in both SecurityContext - and PodSecurityContext, the value specified in SecurityContext - takes precedence. Note that this field cannot be set when spec.os.name - is windows. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer seLinuxOptions: - description: The SELinux context to be applied to the container. - If unspecified, the container runtime will allocate a random - SELinux context for each container. May also be set in PodSecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence. Note that this - field cannot be set when spec.os.name is windows. + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. properties: level: description: Level is SELinux level label that applies to @@ -1074,70 +1015,72 @@ spec: type: string type: object seccompProfile: - description: The seccomp options to use by this container. If - seccomp options are provided at both the pod & container level, - the container options override the pod options. Note that this - field cannot be set when spec.os.name is windows. + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. properties: localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. The profile must be - preconfigured on the node to work. Must be a descending - path, relative to the kubelet's configured seccomp profile - location. Must only be set if type is "Localhost". + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. type: string type: - description: "type indicates which kind of seccomp profile - will be applied. Valid options are: \n Localhost - a profile - defined in a file on the node should be used. RuntimeDefault - - the container runtime default profile should be used. - Unconfined - no profile should be applied." + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. type: string required: - type type: object windowsOptions: - description: The Windows specific settings applied to all containers. - If unspecified, the options from the PodSecurityContext will - be used. If set in both SecurityContext and PodSecurityContext, - the value specified in SecurityContext takes precedence. Note - that this field cannot be set when spec.os.name is linux. + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. properties: gmsaCredentialSpec: - description: GMSACredentialSpec is where the GMSA admission - webhook (https://github.com/kubernetes-sigs/windows-gmsa) - inlines the contents of the GMSA credential spec named by - the GMSACredentialSpecName field. + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. type: string gmsaCredentialSpecName: description: GMSACredentialSpecName is the name of the GMSA credential spec to use. type: string hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. This field is alpha-level - and will only be honored by components that enable the WindowsHostProcessContainers - feature flag. Setting this field without the feature flag - will result in errors when validating the Pod. All of a - Pod's containers must have the same effective HostProcess - value (it is not allowed to have a mix of HostProcess containers - and non-HostProcess containers). In addition, if HostProcess - is true then HostNetwork must also be set to true. + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. type: boolean runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. Defaults to the user specified - in image metadata if unspecified. May also be set in PodSecurityContext. - If set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: string type: object type: object + enableActiveRedis: + description: EnableActiveRedis enable active-active model for Redis + type: boolean enableTLS: - description: EnableTLS enable TLS for redis + description: EnableTLS type: boolean env: - description: Env inject envs to redis pods. + description: Env is the environment variables items: description: EnvVar represents an environment variable present in a Container. @@ -1146,15 +1089,16 @@ spec: description: Name of the environment variable. Must be a C_IDENTIFIER. type: string value: - description: 'Variable references $(VAR_NAME) are expanded using - the previously defined environment variables in the container - and any service environment variables. If a variable cannot - be resolved, the reference in the input string will be unchanged. - Double $$ are reduced to a single $, which allows for escaping - the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the - string literal "$(VAR_NAME)". Escaped references will never - be expanded, regardless of whether the variable exists or - not. Defaults to "".' + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string valueFrom: description: Source for the environment variable's value. Cannot @@ -1167,8 +1111,9 @@ spec: description: The key to select. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string optional: description: Specify whether the ConfigMap or its key @@ -1179,10 +1124,9 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: 'Selects a field of the pod: supports metadata.name, - metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, - status.podIP, status.podIPs.' + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: description: Version of the schema the FieldPath is @@ -1197,10 +1141,9 @@ spec: type: object x-kubernetes-map-type: atomic resourceFieldRef: - description: 'Selects a resource of the container: only - resources limits and requests (limits.cpu, limits.memory, - limits.ephemeral-storage, requests.cpu, requests.memory - and requests.ephemeral-storage) are currently supported.' + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. properties: containerName: description: 'Container name: required for volumes, @@ -1229,8 +1172,9 @@ spec: be a valid secret key. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string optional: description: Specify whether the Secret or its key must @@ -1249,47 +1193,67 @@ spec: description: Expose config for service access properties: accessPort: - description: AccessPort lb service access port + description: AccessPort defines the lb access nodeport format: int32 type: integer + annotations: + additionalProperties: + type: string + description: The annnotations of the service which attached to + services + type: object dataStorageNodePortMap: additionalProperties: format: int32 type: integer - description: DataStorageNodePortMap redis port map referred by - pod name + description: |- + NodePortMap defines the map of the nodeport for redis sentinel only + Reversed for 3.14 backward compatibility type: object dataStorageNodePortSequence: - description: DataStorageNodePortSequence redis port list separated - by commas + description: |- + NodePortMap defines the map of the nodeport for redis nodes + NodePortSequence defines the sequence of the nodeport for redis cluster only type: string - enableNodePort: - description: EnableNodePort enable nodeport - type: boolean - exposeImage: - description: ExposeImage expose image + image: + description: Image defines the image used to expose redis from + annotations + type: string + imagePullPolicy: + description: ImagePullPolicy defines the image pull policy + type: string + ipFamilyPrefer: + description: IPFamily represents the IP Family (IPv4 or IPv6). + This type is used to express the family of an IP expressed by + a type (e.g. service.spec.ipFamilies). + enum: + - IPv4 + - IPv6 + type: string + type: + description: ServiceType defines the type of the all related service + enum: + - NodePort + - LoadBalancer + - ClusterIP type: string type: object image: - description: Image is the Redis image to run. + description: Image is the Redis image type: string imagePullPolicy: - description: ImagePullPolicy is the pull policy for the Redis image. - enum: - - Always - - Never - - IfNotPresent + description: ImagePullPolicy is the Redis image pull policy type: string imagePullSecrets: - description: ImagePullSecrets is the list of pull secrets for the - Redis image. items: - description: LocalObjectReference contains enough information to - let you locate the referenced object inside the same namespace. + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. properties: name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string type: object x-kubernetes-map-type: atomic @@ -1298,26 +1262,29 @@ spec: description: 'IPFamilyPrefer the prefered IP family, enum: IPv4, IPv6' type: string masterSize: - description: MasterSize is the number of shards + description: MasterSize is the number of master nodes format: int32 + minimum: 3 type: integer monitor: description: Monitor properties: args: - description: 'Arguments to the entrypoint. The docker image''s - CMD is used if this is not provided. Variable references $(VAR_NAME) - are expanded using the container''s environment. If a variable - cannot be resolved, the reference in the input string will be - unchanged. The $(VAR_NAME) syntax can be escaped with a double - $$, ie: $$(VAR_NAME). Escaped references will never be expanded, - regardless of whether the variable exists or not. Cannot be - updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + description: |- + Arguments to the entrypoint. + The docker image's CMD is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + cannot be resolved, the reference in the input string will be unchanged. The $(VAR_NAME) syntax + can be escaped with a double $$, ie: $$(VAR_NAME). Escaped references will never be expanded, + regardless of whether the variable exists or not. + Cannot be updated. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell items: type: string type: array env: - description: List of environment variables to set in the container. + description: |- + List of environment variables to set in the container. Cannot be updated. items: description: EnvVar represents an environment variable present @@ -1328,15 +1295,16 @@ spec: C_IDENTIFIER. type: string value: - description: 'Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in - the container and any service environment variables. If - a variable cannot be resolved, the reference in the input - string will be unchanged. Double $$ are reduced to a single - $, which allows for escaping the $(VAR_NAME) syntax: i.e. + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless - of whether the variable exists or not. Defaults to "".' + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string valueFrom: description: Source for the environment variable's value. @@ -1349,9 +1317,9 @@ spec: description: The key to select. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string optional: description: Specify whether the ConfigMap or its @@ -1362,11 +1330,9 @@ spec: type: object x-kubernetes-map-type: atomic fieldRef: - description: 'Selects a field of the pod: supports metadata.name, - metadata.namespace, `metadata.labels['''']`, - `metadata.annotations['''']`, spec.nodeName, - spec.serviceAccountName, status.hostIP, status.podIP, - status.podIPs.' + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: description: Version of the schema the FieldPath @@ -1381,10 +1347,9 @@ spec: type: object x-kubernetes-map-type: atomic resourceFieldRef: - description: 'Selects a resource of the container: only - resources limits and requests (limits.cpu, limits.memory, - limits.ephemeral-storage, requests.cpu, requests.memory - and requests.ephemeral-storage) are currently supported.' + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. properties: containerName: description: 'Container name: required for volumes, @@ -1414,9 +1379,9 @@ spec: be a valid secret key. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string optional: description: Specify whether the Secret or its key @@ -1432,26 +1397,56 @@ spec: type: object type: array image: - description: Image monitor image type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + prometheus: + description: "PrometheusSpec\n\n\tthis struct must be Deprecated, + only port is used." + properties: + interval: + description: Interval at which metrics should be scraped + type: string + labels: + additionalProperties: + type: string + description: Labels are key value pairs that is used to select + Prometheus instance via ServiceMonitor labels. + type: object + namespace: + description: Namespace of Prometheus. Service monitors will + be created in this namespace. + type: string + port: + description: Port number for the exporter side car. + format: int32 + type: integer + type: object resources: - description: 'Compute Resources required by exporter container. - Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + description: |- + Compute Resources required by exporter container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ properties: claims: - description: "Claims lists the names of resources, defined - in spec.resourceClaims, that are used by this container. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: Name must match the name of one entry in - pod.spec.resourceClaims of the Pod where this field - is used. It makes that resource available inside a - container. + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. type: string required: - name @@ -1467,8 +1462,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -1477,32 +1473,34 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed - Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object securityContext: - description: 'Security options the pod should run with. More info: - https://kubernetes.io/docs/concepts/policy/security-context/ - More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + description: |- + Security options the pod should run with. + More info: https://kubernetes.io/docs/concepts/policy/security-context/ + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ properties: allowPrivilegeEscalation: - description: 'AllowPrivilegeEscalation controls whether a - process can gain more privileges than its parent process. - This bool directly controls if the no_new_privs flag will - be set on the container process. AllowPrivilegeEscalation - is true always when the container is: 1) run as Privileged - 2) has CAP_SYS_ADMIN Note that this field cannot be set - when spec.os.name is windows.' + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. type: boolean capabilities: - description: The capabilities to add/drop when running containers. - Defaults to the default set of capabilities granted by the - container runtime. Note that this field cannot be set when - spec.os.name is windows. + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. properties: add: description: Added capabilities @@ -1520,59 +1518,60 @@ spec: type: array type: object privileged: - description: Run container in privileged mode. Processes in - privileged containers are essentially equivalent to root - on the host. Defaults to false. Note that this field cannot - be set when spec.os.name is windows. + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. type: boolean procMount: - description: procMount denotes the type of proc mount to use - for the containers. The default is DefaultProcMount which - uses the container runtime defaults for readonly paths and - masked paths. This requires the ProcMountType feature flag - to be enabled. Note that this field cannot be set when spec.os.name - is windows. + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. type: string readOnlyRootFilesystem: - description: Whether this container has a read-only root filesystem. - Default is false. Note that this field cannot be set when - spec.os.name is windows. + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. type: boolean runAsGroup: - description: The GID to run the entrypoint of the container - process. Uses runtime default if unset. May also be set - in PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext - takes precedence. Note that this field cannot be set when - spec.os.name is windows. + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer runAsNonRoot: - description: Indicates that the container must run as a non-root - user. If true, the Kubelet will validate the image at runtime - to ensure that it does not run as UID 0 (root) and fail - to start the container if it does. If unset or false, no - such validation will be performed. May also be set in PodSecurityContext. If - set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: boolean runAsUser: - description: The UID to run the entrypoint of the container - process. Defaults to user specified in image metadata if - unspecified. May also be set in PodSecurityContext. If - set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. Note - that this field cannot be set when spec.os.name is windows. + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer seLinuxOptions: - description: The SELinux context to be applied to the container. - If unspecified, the container runtime will allocate a random - SELinux context for each container. May also be set in - PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext - takes precedence. Note that this field cannot be set when - spec.os.name is windows. + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. properties: level: description: Level is SELinux level label that applies @@ -1592,65 +1591,61 @@ spec: type: string type: object seccompProfile: - description: The seccomp options to use by this container. - If seccomp options are provided at both the pod & container - level, the container options override the pod options. Note - that this field cannot be set when spec.os.name is windows. + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. properties: localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. The profile must - be preconfigured on the node to work. Must be a descending - path, relative to the kubelet's configured seccomp profile - location. Must only be set if type is "Localhost". + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. type: string type: - description: "type indicates which kind of seccomp profile - will be applied. Valid options are: \n Localhost - a - profile defined in a file on the node should be used. - RuntimeDefault - the container runtime default profile - should be used. Unconfined - no profile should be applied." + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. type: string required: - type type: object windowsOptions: - description: The Windows specific settings applied to all - containers. If unspecified, the options from the PodSecurityContext - will be used. If set in both SecurityContext and PodSecurityContext, - the value specified in SecurityContext takes precedence. - Note that this field cannot be set when spec.os.name is - linux. + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. properties: gmsaCredentialSpec: - description: GMSACredentialSpec is where the GMSA admission - webhook (https://github.com/kubernetes-sigs/windows-gmsa) - inlines the contents of the GMSA credential spec named - by the GMSACredentialSpecName field. + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. type: string gmsaCredentialSpecName: description: GMSACredentialSpecName is the name of the GMSA credential spec to use. type: string hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. This field is - alpha-level and will only be honored by components that - enable the WindowsHostProcessContainers feature flag. - Setting this field without the feature flag will result - in errors when validating the Pod. All of a Pod's containers - must have the same effective HostProcess value (it is - not allowed to have a mix of HostProcess containers - and non-HostProcess containers). In addition, if HostProcess - is true then HostNetwork must also be set to true. + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. type: boolean runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. Defaults to the user specified - in image metadata if unspecified. May also be set in - PodSecurityContext. If set in both SecurityContext and - PodSecurityContext, the value specified in SecurityContext - takes precedence. + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: string type: object type: object @@ -1661,30 +1656,41 @@ spec: description: NodeSelector type: object passwordSecret: - description: PasswordSecret password secret for redis pods + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. properties: name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string type: object x-kubernetes-map-type: atomic + requiredAntiAffinity: + description: Set RequiredAntiAffinity to force the master-slave node + anti-affinity. + type: boolean resources: - description: StorageType storage type for redis pods + description: ResourceRequirements describes the compute resource requirements. properties: claims: - description: "Claims lists the names of resources, defined in - spec.resourceClaims, that are used by this container. \n This - is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be set - for containers." + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. properties: name: - description: Name must match the name of one entry in pod.spec.resourceClaims - of the Pod where this field is used. It makes that resource - available inside a container. + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. type: string required: - name @@ -1700,8 +1706,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute resources - allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -1710,83 +1717,91 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object restore: description: Restore restore redis data from backup properties: backupName: - description: BackupName is the backup cr name to restore. type: string image: - description: Image is the Redis restore image to run. type: string imagePullPolicy: - description: ImagePullPolicy is the Image pull policy. + description: PullPolicy describes a policy for if/when to pull + a container image type: string type: object securityContext: - description: SecurityContext for redis pods + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. properties: fsGroup: - description: "A special supplemental group that applies to all - containers in a pod. Some volume types allow the Kubelet to - change the ownership of that volume to be owned by the pod: - \n 1. The owning GID will be the FSGroup 2. The setgid bit is - set (new files created in the volume will be owned by FSGroup) - 3. The permission bits are OR'd with rw-rw---- \n If unset, - the Kubelet will not modify the ownership and permissions of - any volume. Note that this field cannot be set when spec.os.name - is windows." + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer fsGroupChangePolicy: - description: 'fsGroupChangePolicy defines behavior of changing - ownership and permission of the volume before being exposed - inside Pod. This field will only apply to volume types which - support fsGroup based ownership(and permissions). It will have - no effect on ephemeral volume types such as: secret, configmaps - and emptydir. Valid values are "OnRootMismatch" and "Always". - If not specified, "Always" is used. Note that this field cannot - be set when spec.os.name is windows.' + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. type: string runAsGroup: - description: The GID to run the entrypoint of the container process. - Uses runtime default if unset. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer runAsNonRoot: - description: Indicates that the container must run as a non-root - user. If true, the Kubelet will validate the image at runtime - to ensure that it does not run as UID 0 (root) and fail to start - the container if it does. If unset or false, no such validation - will be performed. May also be set in SecurityContext. If set - in both SecurityContext and PodSecurityContext, the value specified - in SecurityContext takes precedence. + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: boolean runAsUser: - description: The UID to run the entrypoint of the container process. + description: |- + The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. - May also be set in SecurityContext. If set in both SecurityContext - and PodSecurityContext, the value specified in SecurityContext - takes precedence for that container. Note that this field cannot - be set when spec.os.name is windows. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer seLinuxOptions: - description: The SELinux context to be applied to all containers. - If unspecified, the container runtime will allocate a random - SELinux context for each container. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. Note that this field cannot be set when spec.os.name is windows. properties: level: @@ -1807,46 +1822,47 @@ spec: type: string type: object seccompProfile: - description: The seccomp options to use by the containers in this - pod. Note that this field cannot be set when spec.os.name is - windows. + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. properties: localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. The profile must be - preconfigured on the node to work. Must be a descending - path, relative to the kubelet's configured seccomp profile - location. Must only be set if type is "Localhost". + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. type: string type: - description: "type indicates which kind of seccomp profile - will be applied. Valid options are: \n Localhost - a profile - defined in a file on the node should be used. RuntimeDefault - - the container runtime default profile should be used. - Unconfined - no profile should be applied." + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. type: string required: - type type: object supplementalGroups: - description: A list of groups applied to the first process run - in each container, in addition to the container's primary GID, - the fsGroup (if specified), and group memberships defined in - the container image for the uid of the container process. If - unspecified, no additional groups are added to any container. - Note that group memberships defined in the container image for - the uid of the container process are still effective, even if - they are not included in this list. Note that this field cannot - be set when spec.os.name is windows. + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. items: format: int64 type: integer type: array sysctls: - description: Sysctls hold a list of namespaced sysctls used for - the pod. Pods with unsupported sysctls (by the container runtime) - might fail to launch. Note that this field cannot be set when - spec.os.name is windows. + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. items: description: Sysctl defines a kernel parameter to be set properties: @@ -1862,58 +1878,59 @@ spec: type: object type: array windowsOptions: - description: The Windows specific settings applied to all containers. - If unspecified, the options within a container's SecurityContext - will be used. If set in both SecurityContext and PodSecurityContext, - the value specified in SecurityContext takes precedence. Note - that this field cannot be set when spec.os.name is linux. + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. properties: gmsaCredentialSpec: - description: GMSACredentialSpec is where the GMSA admission - webhook (https://github.com/kubernetes-sigs/windows-gmsa) - inlines the contents of the GMSA credential spec named by - the GMSACredentialSpecName field. + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. type: string gmsaCredentialSpecName: description: GMSACredentialSpecName is the name of the GMSA credential spec to use. type: string hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. This field is alpha-level - and will only be honored by components that enable the WindowsHostProcessContainers - feature flag. Setting this field without the feature flag - will result in errors when validating the Pod. All of a - Pod's containers must have the same effective HostProcess - value (it is not allowed to have a mix of HostProcess containers - and non-HostProcess containers). In addition, if HostProcess - is true then HostNetwork must also be set to true. + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. type: boolean runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. Defaults to the user specified - in image metadata if unspecified. May also be set in PodSecurityContext. - If set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. type: string type: object type: object + serviceID: + description: ServiceID the service id for activeredis + format: int32 + maximum: 15 + minimum: 0 + type: integer serviceMonitor: - description: ServiceMonitor + description: |- + ServiceMonitor + not support setup service monitor for each instance properties: customMetricRelabelings: - description: CustomMetricRelabelings custom metric relabelings type: boolean interval: - description: Interval type: string metricRelabelings: - description: MetricRelabelConfigs metric relabel configs items: - description: 'RelabelConfig allows dynamic rewriting of the - label set, being applied to samples before ingestion. It defines - ``-section of Prometheus configuration. - More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs' + description: |- + RelabelConfig allows dynamic rewriting of the label set, being applied to samples before ingestion. + It defines ``-section of Prometheus configuration. + More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs properties: action: default: replace @@ -1938,18 +1955,18 @@ spec: value is matched. Default is '(.*)' type: string replacement: - description: Replacement value against which a regex replace - is performed if the regular expression matches. Regex - capture groups are available. Default is '$1' + description: |- + Replacement value against which a regex replace is performed if the + regular expression matches. Regex capture groups are available. Default is '$1' type: string separator: description: Separator placed between concatenated source label values. default is ';'. type: string sourceLabels: - description: The source labels select values from existing - labels. Their content is concatenated using the configured - separator and matched against the configured regular expression + description: |- + The source labels select values from existing labels. Their content is concatenated + using the configured separator and matched against the configured regular expression for the replace, keep, and drop actions. items: description: LabelName is a valid Prometheus label name @@ -1959,23 +1976,22 @@ spec: type: string type: array targetLabel: - description: Label to which the resulting value is written - in a replace action. It is mandatory for replace actions. - Regex capture groups are available. + description: |- + Label to which the resulting value is written in a replace action. + It is mandatory for replace actions. Regex capture groups are available. type: string type: object type: array scrapeTimeout: - description: ScrapeTimeout type: string type: object serviceName: - description: ServiceName is the name of the statefulset + description: ServiceName is the service name type: string shards: - description: This field specifies the assignment of cluster shard - slots. this config is only works for new create instance, update - will not take effect after instance is startup + description: |- + This field specifies the assignment of cluster shard slots. + this config is only works for new create instance, update will not take effect after instance is startup items: properties: slots: @@ -1985,7 +2001,8 @@ spec: type: object type: array storage: - description: Storage storage config for redis pods + description: RedisStorage defines the structure used to store the + Redis Data properties: class: type: string @@ -2007,43 +2024,45 @@ spec: tolerations: description: Tolerations items: - description: The pod this Toleration is attached to tolerates any - taint that matches the triple using the matching - operator . + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . properties: effect: - description: Effect indicates the taint effect to match. Empty - means match all taint effects. When specified, allowed values - are NoSchedule, PreferNoSchedule and NoExecute. + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. type: string key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match all - values and all keys. + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. type: string operator: - description: Operator represents a key's relationship to the - value. Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod - can tolerate all taints of a particular category. + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. type: string tolerationSeconds: - description: TolerationSeconds represents the period of time - the toleration (which must be of effect NoExecute, otherwise - this field is ignored) tolerates the taint. By default, it - is not set, which means tolerate the taint forever (do not - evict). Zero and negative values will be treated as 0 (evict - immediately) by the system. + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. format: int64 type: integer value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. type: string type: object type: array + required: + - clusterReplicas + - masterSize type: object status: description: DistributedRedisClusterStatus defines the observed state @@ -2052,6 +2071,49 @@ spec: clusterStatus: description: ClusterStatus the cluster status type: string + detailedStatusRef: + description: DetailedStatusRef detailed status resource ref + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic maxReplicationFactor: description: MaxReplicationFactor the max replication factor format: int32 @@ -2063,39 +2125,41 @@ spec: nodes: description: Nodes the redis cluster nodes items: - description: RedisClusterNode represent a RedisCluster Node + description: RedisNode represent a RedisCluster Node properties: id: - description: ID id of redis-server + description: ID is the redis cluster node id, not runid type: string ip: - description: IP current pod ip + description: IP is the ip of the node. if access announce is + enabled, it will be the access ip type: string masterRef: - description: MasterRef referred to the master node + description: MasterRef is the master node id of this node type: string nodeName: - description: NodeName node name the pod hosted + description: NodeName is the node name of the node where holds + the pod type: string podName: - description: PodName pod name + description: PodName current pod name type: string port: - description: Port current pod port + description: Port is the port of the node. if access announce + is enabled, it will be the access port type: string role: - description: Role redis-server role + description: Role is the role of the node, master or slave type: string slots: - description: Slots this master node holds + description: 'Slots is the slot range for the shard, eg: 0-1000,1002,1005-1100' items: type: string type: array statefulSet: - description: StatefulSet the statefulset current pod belongs + description: StatefulSet is the statefulset name of this pod type: string required: - - id - ip - nodeName - podName @@ -2115,7 +2179,7 @@ spec: description: Reason the reason of the status type: string shards: - description: Shards the shards status + description: Shards the cluster shards items: description: ClusterShards properties: diff --git a/config/crd/bases/redis.middleware.alauda.io_redisbackups.yaml b/config/crd/bases/redis.middleware.alauda.io_redisbackups.yaml deleted file mode 100644 index b855a7c..0000000 --- a/config/crd/bases/redis.middleware.alauda.io_redisbackups.yaml +++ /dev/null @@ -1,1251 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.12.0 - name: redisbackups.redis.middleware.alauda.io -spec: - group: redis.middleware.alauda.io - names: - kind: RedisBackup - listKind: RedisBackupList - plural: redisbackups - singular: redisbackup - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: RedisBackup is the Schema for the redisbackups API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: RedisBackupSpec defines the desired state of RedisBackup - properties: - activeDeadlineSeconds: - description: ActiveDeadlineSeconds active deadline seconds for the - job - format: int64 - type: integer - affinity: - description: Affinity affinity for the job - properties: - nodeAffinity: - description: Describes node affinity scheduling rules for the - pod. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the affinity expressions specified by - this field, but it may choose a node that violates one or - more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node matches - the corresponding matchExpressions; the node(s) with the - highest sum are the most preferred. - items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects (i.e. - is also a no-op). - properties: - preference: - description: A node selector term, associated with the - corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the corresponding - nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this - field are not met at scheduling time, the pod will not be - scheduled onto the node. If the affinity requirements specified - by this field cease to be met at some point during pod execution - (e.g. due to an update), the system may or may not try to - eventually evict the pod from its node. - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: A null or empty node selector term matches - no objects. The requirements of them are ANDed. The - TopologySelectorTerm type implements a subset of the - NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - type: array - required: - - nodeSelectorTerms - type: object - x-kubernetes-map-type: atomic - type: object - podAffinity: - description: Describes pod affinity scheduling rules (e.g. co-locate - this pod in the same node, zone, etc. as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the affinity expressions specified by - this field, but it may choose a node that violates one or - more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node has - pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the corresponding - podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this - field are not met at scheduling time, the pod will not be - scheduled onto the node. If the affinity requirements specified - by this field cease to be met at some point during pod execution - (e.g. due to a pod label update), the system may or may - not try to eventually evict the pod from its node. When - there are multiple elements, the lists of nodes corresponding - to each podAffinityTerm are intersected, i.e. all terms - must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not co-located - (anti-affinity) with, where co-located is defined as running - on a node whose value of the label with key - matches that of any node on which a pod of the set of - pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied to the - union of the namespaces selected by this field and - the ones listed in the namespaces field. null selector - and null or empty namespaces list means "this pod's - namespace". An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace - names that the term applies to. The term is applied - to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. null or - empty namespaces list and null namespaceSelector means - "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where - co-located is defined as running on a node whose value - of the label with key topologyKey matches that of - any node on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules (e.g. - avoid putting this pod in the same node, zone, etc. as some - other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the anti-affinity expressions specified - by this field, but it may choose a node that violates one - or more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling anti-affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node has - pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the corresponding - podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the anti-affinity requirements - specified by this field cease to be met at some point during - pod execution (e.g. due to a pod label update), the system - may or may not try to eventually evict the pod from its - node. When there are multiple elements, the lists of nodes - corresponding to each podAffinityTerm are intersected, i.e. - all terms must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not co-located - (anti-affinity) with, where co-located is defined as running - on a node whose value of the label with key - matches that of any node on which a pod of the set of - pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied to the - union of the namespaces selected by this field and - the ones listed in the namespaces field. null selector - and null or empty namespaces list means "this pod's - namespace". An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace - names that the term applies to. The term is applied - to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. null or - empty namespaces list and null namespaceSelector means - "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where - co-located is defined as running on a node whose value - of the label with key topologyKey matches that of - any node on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - type: object - backoffLimit: - description: BackoffLimit backoff limit for the job - format: int32 - type: integer - image: - description: Image - type: string - imagePullSecrets: - description: ImagePullSecrets image pull secrets for the job - items: - description: LocalObjectReference contains enough information to - let you locate the referenced object inside the same namespace. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object - x-kubernetes-map-type: atomic - type: array - nodeSelector: - additionalProperties: - type: string - description: NodeSelector node selector for the job - type: object - priorityClassName: - description: PriorityClassName priority class name for the job - type: string - resources: - description: Resources resource requirements for the job - properties: - claims: - description: "Claims lists the names of resources, defined in - spec.resourceClaims, that are used by this container. \n This - is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be set - for containers." - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of one entry in pod.spec.resourceClaims - of the Pod where this field is used. It makes that resource - available inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute resources - allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - restoreCreateAt: - description: RestoreCreateAt restore create at - format: date-time - type: string - securityContext: - description: SecurityContext security context for the job - properties: - fsGroup: - description: "A special supplemental group that applies to all - containers in a pod. Some volume types allow the Kubelet to - change the ownership of that volume to be owned by the pod: - \n 1. The owning GID will be the FSGroup 2. The setgid bit is - set (new files created in the volume will be owned by FSGroup) - 3. The permission bits are OR'd with rw-rw---- \n If unset, - the Kubelet will not modify the ownership and permissions of - any volume. Note that this field cannot be set when spec.os.name - is windows." - format: int64 - type: integer - fsGroupChangePolicy: - description: 'fsGroupChangePolicy defines behavior of changing - ownership and permission of the volume before being exposed - inside Pod. This field will only apply to volume types which - support fsGroup based ownership(and permissions). It will have - no effect on ephemeral volume types such as: secret, configmaps - and emptydir. Valid values are "OnRootMismatch" and "Always". - If not specified, "Always" is used. Note that this field cannot - be set when spec.os.name is windows.' - type: string - runAsGroup: - description: The GID to run the entrypoint of the container process. - Uses runtime default if unset. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - runAsNonRoot: - description: Indicates that the container must run as a non-root - user. If true, the Kubelet will validate the image at runtime - to ensure that it does not run as UID 0 (root) and fail to start - the container if it does. If unset or false, no such validation - will be performed. May also be set in SecurityContext. If set - in both SecurityContext and PodSecurityContext, the value specified - in SecurityContext takes precedence. - type: boolean - runAsUser: - description: The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - May also be set in SecurityContext. If set in both SecurityContext - and PodSecurityContext, the value specified in SecurityContext - takes precedence for that container. Note that this field cannot - be set when spec.os.name is windows. - format: int64 - type: integer - seLinuxOptions: - description: The SELinux context to be applied to all containers. - If unspecified, the container runtime will allocate a random - SELinux context for each container. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. - Note that this field cannot be set when spec.os.name is windows. - properties: - level: - description: Level is SELinux level label that applies to - the container. - type: string - role: - description: Role is a SELinux role label that applies to - the container. - type: string - type: - description: Type is a SELinux type label that applies to - the container. - type: string - user: - description: User is a SELinux user label that applies to - the container. - type: string - type: object - seccompProfile: - description: The seccomp options to use by the containers in this - pod. Note that this field cannot be set when spec.os.name is - windows. - properties: - localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. The profile must be - preconfigured on the node to work. Must be a descending - path, relative to the kubelet's configured seccomp profile - location. Must only be set if type is "Localhost". - type: string - type: - description: "type indicates which kind of seccomp profile - will be applied. Valid options are: \n Localhost - a profile - defined in a file on the node should be used. RuntimeDefault - - the container runtime default profile should be used. - Unconfined - no profile should be applied." - type: string - required: - - type - type: object - supplementalGroups: - description: A list of groups applied to the first process run - in each container, in addition to the container's primary GID, - the fsGroup (if specified), and group memberships defined in - the container image for the uid of the container process. If - unspecified, no additional groups are added to any container. - Note that group memberships defined in the container image for - the uid of the container process are still effective, even if - they are not included in this list. Note that this field cannot - be set when spec.os.name is windows. - items: - format: int64 - type: integer - type: array - sysctls: - description: Sysctls hold a list of namespaced sysctls used for - the pod. Pods with unsupported sysctls (by the container runtime) - might fail to launch. Note that this field cannot be set when - spec.os.name is windows. - items: - description: Sysctl defines a kernel parameter to be set - properties: - name: - description: Name of a property to set - type: string - value: - description: Value of a property to set - type: string - required: - - name - - value - type: object - type: array - windowsOptions: - description: The Windows specific settings applied to all containers. - If unspecified, the options within a container's SecurityContext - will be used. If set in both SecurityContext and PodSecurityContext, - the value specified in SecurityContext takes precedence. Note - that this field cannot be set when spec.os.name is linux. - properties: - gmsaCredentialSpec: - description: GMSACredentialSpec is where the GMSA admission - webhook (https://github.com/kubernetes-sigs/windows-gmsa) - inlines the contents of the GMSA credential spec named by - the GMSACredentialSpecName field. - type: string - gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the GMSA - credential spec to use. - type: string - hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. This field is alpha-level - and will only be honored by components that enable the WindowsHostProcessContainers - feature flag. Setting this field without the feature flag - will result in errors when validating the Pod. All of a - Pod's containers must have the same effective HostProcess - value (it is not allowed to have a mix of HostProcess containers - and non-HostProcess containers). In addition, if HostProcess - is true then HostNetwork must also be set to true. - type: boolean - runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. Defaults to the user specified - in image metadata if unspecified. May also be set in PodSecurityContext. - If set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. - type: string - type: object - type: object - source: - description: Source - properties: - SSLSecretName: - description: SSLSecretName redis ssl secret name - type: string - endPoint: - description: Endpoint redis endpoint - items: - description: IpPort - properties: - address: - type: string - masterName: - type: string - port: - format: int64 - type: integer - type: object - type: array - passwordSecret: - description: PasswordSecret - type: string - redisFailoverName: - description: RedisFailoverName redisfailover name - type: string - redisName: - description: RedisName redis instance name - type: string - sourceType: - description: SourceType redis cluster type - type: string - storageClassName: - description: StorageClassName - type: string - type: object - storage: - anyOf: - - type: integer - - type: string - description: Storage - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - target: - description: Target backup target - properties: - s3Option: - description: S3Option is the S3 backup target configuration. - properties: - bucket: - description: Bucket s3 storage bucket - type: string - dir: - description: Dir s3 storage dir - type: string - s3Secret: - description: S3Secret s3 storage access secret - type: string - type: object - type: object - tolerations: - description: Tolerations tolerations for the job - items: - description: The pod this Toleration is attached to tolerates any - taint that matches the triple using the matching - operator . - properties: - effect: - description: Effect indicates the taint effect to match. Empty - means match all taint effects. When specified, allowed values - are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match all - values and all keys. - type: string - operator: - description: Operator represents a key's relationship to the - value. Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod - can tolerate all taints of a particular category. - type: string - tolerationSeconds: - description: TolerationSeconds represents the period of time - the toleration (which must be of effect NoExecute, otherwise - this field is ignored) tolerates the taint. By default, it - is not set, which means tolerate the taint forever (do not - evict). Zero and negative values will be treated as 0 (evict - immediately) by the system. - format: int64 - type: integer - value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. - type: string - type: object - type: array - type: object - status: - description: RedisBackupStatus defines the observed state of RedisBackup - properties: - completionTime: - description: CompletionTime - format: date-time - type: string - condition: - description: Condition - type: string - destination: - description: Destination where store backup data in - type: string - jobName: - description: JobName - type: string - lastCheckTime: - description: LastCheckTime - format: date-time - type: string - message: - description: show message when backup fail - type: string - startTime: - description: StartTime - format: date-time - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/redis.middleware.alauda.io_redisclusterbackups.yaml b/config/crd/bases/redis.middleware.alauda.io_redisclusterbackups.yaml deleted file mode 100644 index d567def..0000000 --- a/config/crd/bases/redis.middleware.alauda.io_redisclusterbackups.yaml +++ /dev/null @@ -1,1247 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.12.0 - name: redisclusterbackups.redis.middleware.alauda.io -spec: - group: redis.middleware.alauda.io - names: - kind: RedisClusterBackup - listKind: RedisClusterBackupList - plural: redisclusterbackups - singular: redisclusterbackup - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: RedisClusterBackup is the Schema for the redisclusterbackups - API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: RedisClusterBackupSpec defines the desired state of RedisClusterBackup - properties: - activeDeadlineSeconds: - description: ActiveDeadlineSeconds active deadline seconds - format: int64 - type: integer - affinity: - description: Affinity affinity - properties: - nodeAffinity: - description: Describes node affinity scheduling rules for the - pod. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the affinity expressions specified by - this field, but it may choose a node that violates one or - more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node matches - the corresponding matchExpressions; the node(s) with the - highest sum are the most preferred. - items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects (i.e. - is also a no-op). - properties: - preference: - description: A node selector term, associated with the - corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the corresponding - nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this - field are not met at scheduling time, the pod will not be - scheduled onto the node. If the affinity requirements specified - by this field cease to be met at some point during pod execution - (e.g. due to an update), the system may or may not try to - eventually evict the pod from its node. - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: A null or empty node selector term matches - no objects. The requirements of them are ANDed. The - TopologySelectorTerm type implements a subset of the - NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - type: array - required: - - nodeSelectorTerms - type: object - x-kubernetes-map-type: atomic - type: object - podAffinity: - description: Describes pod affinity scheduling rules (e.g. co-locate - this pod in the same node, zone, etc. as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the affinity expressions specified by - this field, but it may choose a node that violates one or - more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node has - pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the corresponding - podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this - field are not met at scheduling time, the pod will not be - scheduled onto the node. If the affinity requirements specified - by this field cease to be met at some point during pod execution - (e.g. due to a pod label update), the system may or may - not try to eventually evict the pod from its node. When - there are multiple elements, the lists of nodes corresponding - to each podAffinityTerm are intersected, i.e. all terms - must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not co-located - (anti-affinity) with, where co-located is defined as running - on a node whose value of the label with key - matches that of any node on which a pod of the set of - pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied to the - union of the namespaces selected by this field and - the ones listed in the namespaces field. null selector - and null or empty namespaces list means "this pod's - namespace". An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace - names that the term applies to. The term is applied - to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. null or - empty namespaces list and null namespaceSelector means - "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where - co-located is defined as running on a node whose value - of the label with key topologyKey matches that of - any node on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules (e.g. - avoid putting this pod in the same node, zone, etc. as some - other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the anti-affinity expressions specified - by this field, but it may choose a node that violates one - or more of the expressions. The node that is most preferred - is the one with the greatest sum of weights, i.e. for each - node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling anti-affinity expressions, - etc.), compute a sum by iterating through the elements of - this field and adding "weight" to the sum if the node has - pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the corresponding - podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the anti-affinity requirements - specified by this field cease to be met at some point during - pod execution (e.g. due to a pod label update), the system - may or may not try to eventually evict the pod from its - node. When there are multiple elements, the lists of nodes - corresponding to each podAffinityTerm are intersected, i.e. - all terms must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not co-located - (anti-affinity) with, where co-located is defined as running - on a node whose value of the label with key - matches that of any node on which a pod of the set of - pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied to the - union of the namespaces selected by this field and - the ones listed in the namespaces field. null selector - and null or empty namespaces list means "this pod's - namespace". An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace - names that the term applies to. The term is applied - to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. null or - empty namespaces list and null namespaceSelector means - "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where - co-located is defined as running on a node whose value - of the label with key topologyKey matches that of - any node on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - type: object - backoffLimit: - description: BackoffLimit backoff limit - format: int32 - type: integer - image: - description: Image backup image - type: string - imagePullSecrets: - description: ImagePullSecrets image pull secrets - items: - description: LocalObjectReference contains enough information to - let you locate the referenced object inside the same namespace. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object - x-kubernetes-map-type: atomic - type: array - nodeSelector: - additionalProperties: - type: string - description: NodeSelector node selector - type: object - priorityClassName: - description: PriorityClassName priority class name - type: string - resources: - description: Resources backup pod resource config - properties: - claims: - description: "Claims lists the names of resources, defined in - spec.resourceClaims, that are used by this container. \n This - is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be set - for containers." - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of one entry in pod.spec.resourceClaims - of the Pod where this field is used. It makes that resource - available inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute resources - allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - securityContext: - description: SecurityContext security context - properties: - fsGroup: - description: "A special supplemental group that applies to all - containers in a pod. Some volume types allow the Kubelet to - change the ownership of that volume to be owned by the pod: - \n 1. The owning GID will be the FSGroup 2. The setgid bit is - set (new files created in the volume will be owned by FSGroup) - 3. The permission bits are OR'd with rw-rw---- \n If unset, - the Kubelet will not modify the ownership and permissions of - any volume. Note that this field cannot be set when spec.os.name - is windows." - format: int64 - type: integer - fsGroupChangePolicy: - description: 'fsGroupChangePolicy defines behavior of changing - ownership and permission of the volume before being exposed - inside Pod. This field will only apply to volume types which - support fsGroup based ownership(and permissions). It will have - no effect on ephemeral volume types such as: secret, configmaps - and emptydir. Valid values are "OnRootMismatch" and "Always". - If not specified, "Always" is used. Note that this field cannot - be set when spec.os.name is windows.' - type: string - runAsGroup: - description: The GID to run the entrypoint of the container process. - Uses runtime default if unset. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. - Note that this field cannot be set when spec.os.name is windows. - format: int64 - type: integer - runAsNonRoot: - description: Indicates that the container must run as a non-root - user. If true, the Kubelet will validate the image at runtime - to ensure that it does not run as UID 0 (root) and fail to start - the container if it does. If unset or false, no such validation - will be performed. May also be set in SecurityContext. If set - in both SecurityContext and PodSecurityContext, the value specified - in SecurityContext takes precedence. - type: boolean - runAsUser: - description: The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - May also be set in SecurityContext. If set in both SecurityContext - and PodSecurityContext, the value specified in SecurityContext - takes precedence for that container. Note that this field cannot - be set when spec.os.name is windows. - format: int64 - type: integer - seLinuxOptions: - description: The SELinux context to be applied to all containers. - If unspecified, the container runtime will allocate a random - SELinux context for each container. May also be set in SecurityContext. If - set in both SecurityContext and PodSecurityContext, the value - specified in SecurityContext takes precedence for that container. - Note that this field cannot be set when spec.os.name is windows. - properties: - level: - description: Level is SELinux level label that applies to - the container. - type: string - role: - description: Role is a SELinux role label that applies to - the container. - type: string - type: - description: Type is a SELinux type label that applies to - the container. - type: string - user: - description: User is a SELinux user label that applies to - the container. - type: string - type: object - seccompProfile: - description: The seccomp options to use by the containers in this - pod. Note that this field cannot be set when spec.os.name is - windows. - properties: - localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. The profile must be - preconfigured on the node to work. Must be a descending - path, relative to the kubelet's configured seccomp profile - location. Must only be set if type is "Localhost". - type: string - type: - description: "type indicates which kind of seccomp profile - will be applied. Valid options are: \n Localhost - a profile - defined in a file on the node should be used. RuntimeDefault - - the container runtime default profile should be used. - Unconfined - no profile should be applied." - type: string - required: - - type - type: object - supplementalGroups: - description: A list of groups applied to the first process run - in each container, in addition to the container's primary GID, - the fsGroup (if specified), and group memberships defined in - the container image for the uid of the container process. If - unspecified, no additional groups are added to any container. - Note that group memberships defined in the container image for - the uid of the container process are still effective, even if - they are not included in this list. Note that this field cannot - be set when spec.os.name is windows. - items: - format: int64 - type: integer - type: array - sysctls: - description: Sysctls hold a list of namespaced sysctls used for - the pod. Pods with unsupported sysctls (by the container runtime) - might fail to launch. Note that this field cannot be set when - spec.os.name is windows. - items: - description: Sysctl defines a kernel parameter to be set - properties: - name: - description: Name of a property to set - type: string - value: - description: Value of a property to set - type: string - required: - - name - - value - type: object - type: array - windowsOptions: - description: The Windows specific settings applied to all containers. - If unspecified, the options within a container's SecurityContext - will be used. If set in both SecurityContext and PodSecurityContext, - the value specified in SecurityContext takes precedence. Note - that this field cannot be set when spec.os.name is linux. - properties: - gmsaCredentialSpec: - description: GMSACredentialSpec is where the GMSA admission - webhook (https://github.com/kubernetes-sigs/windows-gmsa) - inlines the contents of the GMSA credential spec named by - the GMSACredentialSpecName field. - type: string - gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the GMSA - credential spec to use. - type: string - hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. This field is alpha-level - and will only be honored by components that enable the WindowsHostProcessContainers - feature flag. Setting this field without the feature flag - will result in errors when validating the Pod. All of a - Pod's containers must have the same effective HostProcess - value (it is not allowed to have a mix of HostProcess containers - and non-HostProcess containers). In addition, if HostProcess - is true then HostNetwork must also be set to true. - type: boolean - runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. Defaults to the user specified - in image metadata if unspecified. May also be set in PodSecurityContext. - If set in both SecurityContext and PodSecurityContext, the - value specified in SecurityContext takes precedence. - type: string - type: object - type: object - source: - description: Source backup source - properties: - SSLSecretName: - description: SSLSecretName ssl secret name - type: string - endPoint: - description: Endpoint redis cluster endpoint - items: - description: IpPort - properties: - address: - type: string - masterName: - type: string - port: - format: int64 - type: integer - type: object - type: array - passwordSecret: - description: PasswordSecret password secret - type: string - redisClusterName: - description: RedisClusterName redis cluster name - type: string - storageClassName: - description: StorageClassName storage class name - type: string - type: object - storage: - anyOf: - - type: integer - - type: string - description: Storage backup storage - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - target: - description: Target backup target - properties: - grpcOption: - description: GRPCOption grpc option - properties: - secretName: - type: string - type: object - s3Option: - description: S3Option - properties: - bucket: - description: Bucket - type: string - dir: - description: Dir - type: string - s3Secret: - description: S3Secret - type: string - type: object - type: object - tolerations: - description: Tolerations tolerations - items: - description: The pod this Toleration is attached to tolerates any - taint that matches the triple using the matching - operator . - properties: - effect: - description: Effect indicates the taint effect to match. Empty - means match all taint effects. When specified, allowed values - are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match all - values and all keys. - type: string - operator: - description: Operator represents a key's relationship to the - value. Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod - can tolerate all taints of a particular category. - type: string - tolerationSeconds: - description: TolerationSeconds represents the period of time - the toleration (which must be of effect NoExecute, otherwise - this field is ignored) tolerates the taint. By default, it - is not set, which means tolerate the taint forever (do not - evict). Zero and negative values will be treated as 0 (evict - immediately) by the system. - format: int64 - type: integer - value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. - type: string - type: object - type: array - type: object - status: - description: RedisClusterBackupStatus defines the observed state of RedisClusterBackup - properties: - completionTime: - description: CompletionTime completion time - format: date-time - type: string - condition: - description: Condition backup condition - type: string - destination: - description: optional where store backup data in - type: string - jobName: - description: JobName job name run this backup - type: string - lastCheckTime: - description: LastCheckTime last check time - format: date-time - type: string - message: - description: show message when backup fail - type: string - startTime: - description: StartTime start time - format: date-time - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/redis.middleware.alauda.io_redisusers.yaml b/config/crd/bases/redis.middleware.alauda.io_redisusers.yaml index 3f7e3d5..c38eed0 100644 --- a/config/crd/bases/redis.middleware.alauda.io_redisusers.yaml +++ b/config/crd/bases/redis.middleware.alauda.io_redisusers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.16.0 name: redisusers.redis.middleware.alauda.io spec: group: redis.middleware.alauda.io @@ -27,14 +27,19 @@ spec: description: RedisUser is the Schema for the redisusers API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -49,15 +54,14 @@ spec: - default type: string aclRules: - description: AclRules acl rules - maxLength: 4096 + description: redis acl rules string type: string arch: - default: sentinel description: redis user account type enum: - sentinel - cluster + - standalone type: string passwordSecrets: description: Redis Password secret name, key is password @@ -70,8 +74,7 @@ spec: minLength: 1 type: string username: - description: Username - maxLength: 64 + description: Redis Username (required) type: string required: - redisName @@ -81,21 +84,16 @@ spec: description: RedisUserStatus defines the observed state of RedisUser properties: Phase: - description: Phase enum: - Fail - Success - Pending type: string aclRules: - description: AclRules last configed acl rule type: string lastUpdateSuccess: - description: LastUpdatedSuccess is the last time the user was successfully - updated. type: string message: - description: Message type: string type: object type: object diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5e0d2d8..e49eaff 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,29 +3,29 @@ # It should be run by config/default resources: - bases/databases.spotahome.com_redisfailovers.yaml +- bases/databases.spotahome.com_redissentinels.yaml - bases/redis.kun_distributedredisclusters.yaml -- bases/redis.middleware.alauda.io_redisbackups.yaml -- bases/redis.middleware.alauda.io_redisclusterbackups.yaml - bases/redis.middleware.alauda.io_redisusers.yaml +- bases/middleware.alauda.io_redis.yaml +- bases/middleware.alauda.io_imageversions.yaml #+kubebuilder:scaffold:crdkustomizeresource -patches: +patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -#- patches/webhook_in_redisfailovers.yaml -#- patches/webhook_in_redisbackups.yaml -#- patches/webhook_in_distributedredisclusters.yaml -#- patches/webhook_in_redisclusterbackups.yaml -#- patches/webhook_in_redisusers.yaml +#- path: patches/webhook_in_redisfailovers.yaml +#- path: patches/webhook_in_distributedredisclusters.yaml +#- path: patches/webhook_in_redis.yaml +#- path: patches/webhook_in_redisusers.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -#- patches/cainjection_in_redisfailovers.yaml -#- patches/cainjection_in_redisbackups.yaml -#- patches/cainjection_in_distributedredisclusters.yaml -#- patches/cainjection_in_redisclusterbackups.yaml -#- patches/cainjection_in_redisusers.yaml +#- path: patches/cainjection_in_redisfailovers.yaml +#- path: patches/cainjection_in_distributedredisclusters.yaml +#- path: patches/cainjection_in_redis.yaml +#- path: patches/cainjection_in_redisusers.yaml +#- path: patches/cainjection_in_databases_redissentinels.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_redis.middleware.alauda.io_redisbackups.yaml b/config/crd/patches/cainjection_in_middleware_redis.yaml similarity index 84% rename from config/crd/patches/cainjection_in_redis.middleware.alauda.io_redisbackups.yaml rename to config/crd/patches/cainjection_in_middleware_redis.yaml index 96ba32d..c421136 100644 --- a/config/crd/patches/cainjection_in_redis.middleware.alauda.io_redisbackups.yaml +++ b/config/crd/patches/cainjection_in_middleware_redis.yaml @@ -4,4 +4,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: redisbackups.redis.middleware.alauda.io + name: redis.middleware diff --git a/config/crd/patches/cainjection_in_redis.middleware.alauda.io_redisclusterbackups.yaml b/config/crd/patches/cainjection_in_redis.middleware.alauda.io_redisclusterbackups.yaml deleted file mode 100644 index 9da1945..0000000 --- a/config/crd/patches/cainjection_in_redis.middleware.alauda.io_redisclusterbackups.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: redisclusterbackups.redis.middleware.alauda.io diff --git a/config/crd/patches/webhook_in_redis.middleware.alauda.io_redisbackups.yaml b/config/crd/patches/webhook_in_middleware_redis.yaml similarity index 80% rename from config/crd/patches/webhook_in_redis.middleware.alauda.io_redisbackups.yaml rename to config/crd/patches/webhook_in_middleware_redis.yaml index 4f23b9e..a8dea57 100644 --- a/config/crd/patches/webhook_in_redis.middleware.alauda.io_redisbackups.yaml +++ b/config/crd/patches/webhook_in_middleware_redis.yaml @@ -2,7 +2,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: redisbackups.redis.middleware.alauda.io + name: redis.middleware spec: conversion: strategy: Webhook @@ -10,7 +10,7 @@ spec: clientConfig: service: namespace: system - name: webhook-service + name: redis-operator-service path: /convert conversionReviewVersions: - v1 diff --git a/config/crd/patches/webhook_in_redis.middleware.alauda.io_redisclusterbackups.yaml b/config/crd/patches/webhook_in_redis.middleware.alauda.io_redisclusterbackups.yaml deleted file mode 100644 index d28b59d..0000000 --- a/config/crd/patches/webhook_in_redis.middleware.alauda.io_redisclusterbackups.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: redisclusterbackups.redis.middleware.alauda.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 38ccbd3..68b6991 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,16 +1,18 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization images: - name: redis-operator - newName: ghcr.io/alauda/redis-operator - newTag: v1.0.0 + newName: "" commonAnnotations: - operatorVersion: v3.15.0-beta.20.g274dbf5f - redisDefaultImage: redis:6.0-alpine - redisExporterImage: oliver006/redis_exporter:v1.55.0 - redisVersion72Image: redis:7.2-alpine - redisVersion7Image: redis:6.0-alpine - redisVersion62Image: redis:6.0-alpine - redisVersion6Image: redis:6.0-alpine -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization + redisDefaultImage: 'build-harbor.alauda.cn/:' + redisExporterImage: 'build-harbor.alauda.cn/:' + redisProxyImage: 'build-harbor.alauda.cn/:' + redisShakeImage: 'build-harbor.alauda.cn/:' + redisToolsImage: 'build-harbor.alauda.cn/:' + redisVersion5Image: 'build-harbor.alauda.cn/:' + redisVersion6Image: 'build-harbor.alauda.cn/:' + redisVersion62Image: 'build-harbor.alauda.cn/:' + redisVersion72Image: 'build-harbor.alauda.cn/:' + redisVersion74Image: 'build-harbor.alauda.cn/:' diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 162c806..9394065 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -62,22 +62,38 @@ spec: valueFrom: fieldRef: fieldPath: metadata.annotations['redisDefaultImage'] - - name: REDIS_VERSION_6_IMAGE + - name: REDIS_VERSION_5_IMAGE valueFrom: fieldRef: - fieldPath: metadata.annotations['redisVersion6Image'] - - name: REDIS_VERSION_6_2_IMAGE + fieldPath: metadata.annotations['redisVersion5Image'] + - name: REDIS_VERSION_5_VERSION + valueFrom: + fieldRef: + fieldPath: metadata.annotations['redisVersion5Version'] + - name: REDIS_VERSION_6_IMAGE valueFrom: fieldRef: - fieldPath: metadata.annotations['redisVersion62Image'] - - name: REDIS_VERSION_7_IMAGE + fieldPath: metadata.annotations['redisVersion6Image'] + - name: REDIS_VERSION_6_VERSION valueFrom: fieldRef: - fieldPath: metadata.annotations['redisVersion7Image'] + fieldPath: metadata.annotations['redisVersion6Version'] - name: REDIS_VERSION_7_2_IMAGE valueFrom: fieldRef: fieldPath: metadata.annotations['redisVersion72Image'] + - name: REDIS_VERSION_7_2_VERSION + valueFrom: + fieldRef: + fieldPath: metadata.annotations['redisVersion72Version'] + - name: REDIS_VERSION_7_4_IMAGE + valueFrom: + fieldRef: + fieldPath: metadata.annotations['redisVersion74Image'] + - name: REDIS_VERSION_7_4_VERSION + valueFrom: + fieldRef: + fieldPath: metadata.annotations['redisVersion74Version'] - name: DEFAULT_EXPORTER_IMAGE valueFrom: fieldRef: @@ -86,10 +102,10 @@ spec: valueFrom: fieldRef: fieldPath: metadata.annotations['olm.targetNamespaces'] - - name: REDIS_TOOLS_IMAGE + - name: REDIS_TOOLS_IMAGE valueFrom: fieldRef: - fieldPath: spec.containers[0].image + fieldPath: metadata.annotations['redisToolsImage'] - name: POD_NAME valueFrom: fieldRef: @@ -106,3 +122,6 @@ spec: valueFrom: fieldRef: fieldPath: metadata.annotations['operatorVersion'] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true diff --git a/config/manifests/bases/redis-operator.clusterserviceversion.yaml b/config/manifests/bases/redis-operator.clusterserviceversion.yaml index b92ec12..5e662ca 100644 --- a/config/manifests/bases/redis-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/redis-operator.clusterserviceversion.yaml @@ -6,6 +6,8 @@ metadata: labels: operatorframework.io/arch.amd64: supported operatorframework.io/arch.arm64: supported + cpaas.io/protocol.stack.ipv4: supported + cpaas.io/protocol.stack.ipv6: supported name.operator: redis-operator team.operator: dataservices annotations: @@ -23,20 +25,19 @@ metadata: description: "Redis is a high-performance, in-memory data storage system widely used for building highly scalable, fast-responding, real-time data applications." support: 'Alauda' capabilities: Deep Insights - operators.operatorframework.io/builder: operator-sdk-v1.33.0 + operators.operatorframework.io/builder: operator-sdk-v1.2.0 operators.operatorframework.io/project_layout: go + operators.operatorframework.io/disable-create-description: "Please use this function in the Data Services view" olm.skipRange: '>0.3.0 <__CURRENT_VERSION__' spec: - minKubeVersion: "1.17.0" apiservicedefinitions: {} displayName: Redis - description: "# Redis\n## Introduction\nRedis is a high-performance, in-memory data storage system widely used for building highly scalable, fast-responding, real-time data applications.\n## Supported Versions\nRedis 5.0, 6.0, 7.2\n## Supported Features\nThe Redis provided by the platform is fully compatible with the open-source community version, and it has the following advantages:\n1. Out-of-the-box: Based on the Cloud Native Success Platform with cluster, storage, network, and other management capabilities, it minimizes the need to focus on underlying infrastructure. It enables one-click deployment of Kafka instances, visual topic and user management, allowing for rapid integration with business applications.\n2. Unified Operations and Management: Provides a comprehensive set of inspection, logging, monitoring, alerting, and other operational mechanisms.
\ta. Comprehensive health inspection, with alerts and timely recovery for unhealthy states, ensure service availability and business continuity.
\tb. Automatic fault detection and alerts, with the ability to configure monitoring data transmission rules. Receive Kafka load status and exception information via SMS, email, etc., reducing management costs.\n3. Parameter Templates: Provides predefined system parameter templates to help users quickly create Redis instances for maximum performance and stability. Customers can also customize exclusive templates based on the platform's offerings.\n4. Data Security: Provides private delivery, a comprehensive security reinforcement system, including access control, data encryption, and vulnerability fixes, to assist customers in better securing their data.\n5. Updates and Upgrades: Regularly updates Redis versions, including the latest community editions, along with your value-added features and improvements, ensuring users can always access the latest features and fixes.\n6. High Availability and Disaster Recovery: Provides high availability and disaster recovery solutions, along with an SLA consistent with the 'Cloud Native Success Platform,' ensuring the stable operation of users' Redis services and professional support in case of issues.\n7. Multi-Language Support: Kafka client libraries support various programming languages, including Python、Java、Node.js、C#, etc.\n8. Compatible with open-source community version: Applications can be migrated to the cloud without any modifications.

" + description: >+ + "# Redis\n## Introduction\nRedis is a high-performance, in-memory data storage system widely used for building highly scalable, fast-responding, real-time data applications.\n## Supported Versions\nRedis 5.0, 6.0, 7.2\n## Supported Features\nThe Redis provided by the platform is fully compatible with the open-source community version, and it has the following advantages:\n1. Out-of-the-box: Based on the Cloud Native Success Platform with cluster, storage, network, and other management capabilities, it minimizes the need to focus on underlying infrastructure. It enables one-click deployment of Kafka instances, visual topic and user management, allowing for rapid integration with business applications.\n2. Unified Operations and Management: Provides a comprehensive set of inspection, logging, monitoring, alerting, and other operational mechanisms.
\ta. Comprehensive health inspection, with alerts and timely recovery for unhealthy states, ensure service availability and business continuity.
\tb. Automatic fault detection and alerts, with the ability to configure monitoring data transmission rules. Receive Kafka load status and exception information via SMS, email, etc., reducing management costs.\n3. Parameter Templates: Provides predefined system parameter templates to help users quickly create Redis instances for maximum performance and stability. Customers can also customize exclusive templates based on the platform's offerings.\n4. Data Security: Provides private delivery, a comprehensive security reinforcement system, including access control, data encryption, and vulnerability fixes, to assist customers in better securing their data.\n5. Updates and Upgrades: Regularly updates Redis versions, including the latest community editions, along with your value-added features and improvements, ensuring users can always access the latest features and fixes.\n6. High Availability and Disaster Recovery: Provides high availability and disaster recovery solutions, along with an SLA consistent with the 'Cloud Native Success Platform,' ensuring the stable operation of users' Redis services and professional support in case of issues.\n7. Multi-Language Support: Kafka client libraries support various programming languages, including Python、Java、Node.js、C#, etc.\n8. Compatible with open-source community version: Applications can be migrated to the cloud without any modifications.

" maturity: alpha version: 0.0.0 keywords: - Redis - - Sentinel - - Cluster - Database provider: name: System @@ -44,478 +45,13 @@ spec: - base64data: >- PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNTZweCIgaGVpZ2h0PSI0OHB4IiB2aWV3Qm94PSIwIDAgNTYgNDgiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8dGl0bGU+cmVkaXNfd3g0ODwvdGl0bGU+CiAgICA8ZyBpZD0iT3BlcmF0b3JzLeiuvuiuoeaWueahiC3noa7lrprniYgiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSIwMS1BQ1AtMi54Xy3kuK3pl7Tku7Zf5YiX6KGoIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTA4Mi4wMDAwMDAsIC0xNjczLjAwMDAwMCkiIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICAgICAgICAgIDxnIGlkPSJHcm91cC04OCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTkuMDAwMDAwLCAxMTE1LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPGcgaWQ9Ikdyb3VwLTg3IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNDAuMDAwMDAwLCAyNjUuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgPGcgaWQ9Ikdyb3VwLTU4IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCwgMjczLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgICAgICAgICA8ZyBpZD0iR3JvdXAtMTgtQ29weS0yIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDIuMDAwMDAwLCAwLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPGcgaWQ9Ikdyb3VwLTQ5IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMC4wMDAwMDAsIDIwLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxnIGlkPSJyZWRpc193eDQ4IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2MS4wMDAwMDAsIDAuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik01My42NjYxODE4LDM2Ljg2MDI5MDkgQzUwLjY4NTM4MTgsMzguNDEzOTYzNiAzNS4yNDQyMTgyLDQ0Ljc2MjgzNjQgMzEuOTU2ODcyNyw0Ni40NzY2NTQ1IEMyOC42Njk1MjczLDQ4LjE5MDY5MDkgMjYuODQzMzQ1NSw0OC4xNzQxMDkxIDI0LjI0NjMyNzMsNDYuOTMyNjU0NSBDMjEuNjQ5NTI3Myw0NS42OTEyIDUuMjE3MzgxODIsMzkuMDUzNjcyNyAyLjI1NzMwOTA5LDM3LjYzODc2MzYgQzAuNzc4MDM2MzY0LDM2LjkzMTg1NDUgMCwzNi4zMzQ5MDkxIDAsMzUuNzcxMTI3MyBMMCwzMC4xMjU0NTQ1IEMwLDMwLjEyNTQ1NDUgMjEuMzkyNzI3MywyNS40NjgzNjM2IDI0Ljg0NjMyNzMsMjQuMjI5MzA5MSBDMjguMjk5NzA5MSwyMi45OTAyNTQ1IDI5LjQ5Nzk2MzYsMjIuOTQ1NTI3MyAzMi40MzY4NzI3LDI0LjAyMjAzNjQgQzM1LjM3NjIxODIsMjUuMDk4OTgxOCA1Mi45NDk4OTA5LDI4LjI2OTYgNTUuODU0NTQ1NSwyOS4zMzM0NTQ1IEw1NS44NTMyMzY0LDM0Ljg5OTI3MjcgQzU1Ljg1MzY3MjcsMzUuNDU3MzgxOCA1NS4xODM0MTgyLDM2LjA2OTYgNTMuNjY2MTgxOCwzNi44NjAyOTA5IiBpZD0iU2hhcGUiIGZpbGw9IiM5MTI2MjYiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTUzLjY2NTA5MDksMzEuMjQ4IEM1MC42ODQ1MDkxLDMyLjgwMTAxODIgMzUuMjQzNzgxOCwzOS4xNTAxMDkxIDMxLjk1NjQzNjQsNDAuODYzNzA5MSBDMjguNjY5MzA5MSw0Mi41Nzc5NjM2IDI2Ljg0MzEyNzMsNDIuNTYxMTYzNiAyNC4yNDYzMjczLDQxLjMxOTcwOTEgQzIxLjY0OTMwOTEsNDAuMDc5MTI3MyA1LjIxNzgxODE4LDMzLjQ0MDUwOTEgMi4yNTc5NjM2NCwzMi4wMjY0NzI3IEMtMC43MDE4OTA5MDksMzAuNjExNTYzNiAtMC43NjM4NTQ1NDUsMjkuNjM3ODE4MiAyLjE0MzYzNjM2LDI4LjQ5OTM0NTUgQzUuMDUxMTI3MjcsMjcuMzYwNDM2NCAyMS4zOTI1MDkxLDIwLjk0OTE2MzYgMjQuODQ2NTQ1NSwxOS43MTAxMDkxIEMyOC4yOTk5MjczLDE4LjQ3MTQ5MDkgMjkuNDk3OTYzNiwxOC40MjYzMjczIDMyLjQzNjg3MjcsMTkuNTAzMjcyNyBDMzUuMzc2LDIwLjU3OTc4MTggNTAuNzI0NjU0NSwyNi42ODkwOTA5IDUzLjYyODg3MjcsMjcuNzUyNzI3MyBDNTYuNTM0MTgxOCwyOC44MTc2NzI3IDU2LjY0NTY3MjcsMjkuNjk0NTQ1NSA1My42NjUwOTA5LDMxLjI0OCIgaWQ9IlNoYXBlIiBmaWxsPSIjQzYzMDJCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik01My42NjYxODE4LDI3LjcyNTIzNjQgQzUwLjY4NTM4MTgsMjkuMjc5MTI3MyAzNS4yNDQyMTgyLDM1LjYyNzc4MTggMzEuOTU2ODcyNywzNy4zNDIyNTQ1IEMyOC42Njk1MjczLDM5LjA1NTYzNjQgMjYuODQzMzQ1NSwzOS4wMzkwNTQ1IDI0LjI0NjMyNzMsMzcuNzk3NiBDMjEuNjQ5MzA5MSwzNi41NTY4IDUuMjE3MzgxODIsMjkuOTE4NjE4MiAyLjI1NzMwOTA5LDI4LjUwMzcwOTEgQzAuNzc4MDM2MzY0LDI3Ljc5NjggMCwyNy4yMDA3MjczIDAsMjYuNjM2NzI3MyBMMCwyMC45OTA0IEMwLDIwLjk5MDQgMjEuMzkyNzI3MywxNi4zMzM1MjczIDI0Ljg0NjMyNzMsMTUuMDk0NDcyNyBDMjguMjk5NzA5MSwxMy44NTU0MTgyIDI5LjQ5Nzk2MzYsMTMuODEwNDcyNyAzMi40MzY4NzI3LDE0Ljg4NzIgQzM1LjM3NjQzNjQsMTUuOTYzOTI3MyA1Mi45NTAxMDkxLDE5LjEzMzg5MDkgNTUuODU0NTQ1NSwyMC4xOTc5NjM2IEw1NS44NTMyMzY0LDI1Ljc2NDQzNjQgQzU1Ljg1MzY3MjcsMjYuMzIyMzI3MyA1NS4xODM0MTgyLDI2LjkzNDU0NTUgNTMuNjY2MTgxOCwyNy43MjUyMzY0IiBpZD0iU2hhcGUiIGZpbGw9IiM5MTI2MjYiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTUzLjY2NTA5MDksMjIuMTEyOTQ1NSBDNTAuNjg0NTA5MSwyMy42NjY0IDM1LjI0Mzc4MTgsMzAuMDE1MDU0NSAzMS45NTY0MzY0LDMxLjcyOTMwOTEgQzI4LjY2OTMwOTEsMzMuNDQyOTA5MSAyNi44NDMxMjczLDMzLjQyNjEwOTEgMjQuMjQ2MzI3MywzMi4xODQ2NTQ1IEMyMS42NDkzMDkxLDMwLjk0NDA3MjcgNS4yMTc4MTgxOCwyNC4zMDU2NzI3IDIuMjU3OTYzNjQsMjIuODkxNDE4MiBDLTAuNzAxODkwOTA5LDIxLjQ3Njk0NTUgLTAuNzYzODU0NTQ1LDIwLjUwMjk4MTggMi4xNDM2MzYzNiwxOS4zNjQwNzI3IEM1LjA1MTEyNzI3LDE4LjIyNTgxODIgMjEuMzkyNzI3MywxMS44MTQxMDkxIDI0Ljg0NjU0NTUsMTAuNTc1MjcyNyBDMjguMjk5OTI3Myw5LjMzNjQzNjM2IDI5LjQ5Nzk2MzYsOS4yOTE0OTA5MSAzMi40MzY4NzI3LDEwLjM2ODIxODIgQzM1LjM3NiwxMS40NDQ3MjczIDUwLjcyNDY1NDUsMTcuNTUzNiA1My42Mjg4NzI3LDE4LjYxNzY3MjcgQzU2LjUzNDE4MTgsMTkuNjgyNCA1Ni42NDU2NzI3LDIwLjU1OTQ5MDkgNTMuNjY1MDkwOSwyMi4xMTI5NDU1IiBpZD0iU2hhcGUiIGZpbGw9IiNDNjMwMkIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTUzLjY2NjE4MTgsMTguMjUxNTYzNiBDNTAuNjg1MzgxOCwxOS44MDUwMTgyIDM1LjI0NDIxODIsMjYuMTU0MTA5MSAzMS45NTY4NzI3LDI3Ljg2ODU4MTggQzI4LjY2OTUyNzMsMjkuNTgyMTgxOCAyNi44NDMzNDU1LDI5LjU2NTM4MTggMjQuMjQ2MzI3MywyOC4zMjM5MjczIEMyMS42NDkzMDkxLDI3LjA4MzEyNzMgNS4yMTczODE4MiwyMC40NDQ3MjczIDIuMjU3MzA5MDksMTkuMDMwNDcyNyBDMC43NzgwMzYzNjQsMTguMzIyOTA5MSAwLDE3LjcyNjYxODIgMCwxNy4xNjMyNzI3IEwwLDExLjUxNjcyNzMgQzAsMTEuNTE2NzI3MyAyMS4zOTI3MjczLDYuODYwMDcyNzMgMjQuODQ2MzI3Myw1LjYyMTIzNjM2IEMyOC4yOTk3MDkxLDQuMzgxOTYzNjQgMjkuNDk3OTYzNiw0LjMzNzQ1NDU1IDMyLjQzNjg3MjcsNS40MTM5NjM2NCBDMzUuMzc2NDM2NCw2LjQ5MDY5MDkxIDUyLjk1MDEwOTEsOS42NjA2NTQ1NSA1NS44NTQ1NDU1LDEwLjcyNDcyNzMgTDU1Ljg1MzIzNjQsMTYuMjkwOTgxOCBDNTUuODUzNjcyNywxNi44NDg0MzY0IDU1LjE4MzQxODIsMTcuNDYwNjU0NSA1My42NjYxODE4LDE4LjI1MTU2MzYiIGlkPSJTaGFwZSIgZmlsbD0iIzkxMjYyNiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNTMuNjY1MDkwOSwxMi42MzkyNzI3IEM1MC42ODQ1MDkxLDE0LjE5MjcyNzMgMzUuMjQzNzgxOCwyMC41NDE4MTgyIDMxLjk1NjQzNjQsMjIuMjU1NDE4MiBDMjguNjY5MzA5MSwyMy45NjkwMTgyIDI2Ljg0MzEyNzMsMjMuOTUyMjE4MiAyNC4yNDYzMjczLDIyLjcxMTQxODIgQzIxLjY0OTUyNzMsMjEuNDY5OTYzNiA1LjIxNzgxODE4LDE0LjgzMiAyLjI1ODE4MTgyLDEzLjQxNzUyNzMgQy0wLjcwMTg5MDkwOSwxMi4wMDMyNzI3IC0wLjc2MzYzNjM2NCwxMS4wMjkwOTA5IDIuMTQzNjM2MzYsOS44OTA0IEM1LjA1MTEyNzI3LDguNzUxOTI3MjcgMjEuMzkyNzI3MywyLjM0MTA5MDkxIDI0Ljg0NjU0NTUsMS4xMDE4MTgxOCBDMjguMjk5OTI3MywtMC4xMzcyMzYzNjQgMjkuNDk3OTYzNiwtMC4xODE3NDU0NTUgMzIuNDM2ODcyNywwLjg5NDk4MTgxOCBDMzUuMzc2LDEuOTcxNzA5MDkgNTAuNzI0NjU0NSw4LjA4MDU4MTgyIDUzLjYyODg3MjcsOS4xNDQ2NTQ1NSBDNTYuNTM0MTgxOCwxMC4yMDg1MDkxIDU2LjY0NTY3MjcsMTEuMDg1ODE4MiA1My42NjUwOTA5LDEyLjYzOTI3MjciIGlkPSJTaGFwZSIgZmlsbD0iI0M2MzAyQiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMzQuNzUyNjU0NSw3LjE0Njk4MTgyIEwyOS45NTA0NzI3LDcuNjQ1NTI3MjcgTDI4Ljg3NTQ5MDksMTAuMjMyMjkwOSBMMjcuMTM5Miw3LjM0NTc0NTQ1IEwyMS41OTQxMDkxLDYuODQ3NDE4MTggTDI1LjczMTcwOTEsNS4zNTUyNzI3MyBMMjQuNDkwMjU0NSwzLjA2NDggTDI4LjM2NDA3MjcsNC41Nzk4NTQ1NSBMMzIuMDE2LDMuMzg0MjE4MTggTDMxLjAyODk0NTUsNS43NTI1ODE4MiBMMzQuNzUyNjU0NSw3LjE0Njk4MTgyIE0yOC41ODg4LDE5LjY5NjM2MzYgTDE5LjYyNjU0NTUsMTUuOTc5MiBMMzIuNDY4NzI3MywxNC4wMDc5MjczIEwyOC41ODg4LDE5LjY5NjM2MzYgTTE2LjE2MzM0NTUsOC41ODQ4IEMxOS45NTQyNTQ1LDguNTg0OCAyMy4wMjczNDU1LDkuNzc2MDcyNzMgMjMuMDI3MzQ1NSwxMS4yNDUzMDkxIEMyMy4wMjczNDU1LDEyLjcxNDk4MTggMTkuOTU0MjU0NSwxMy45MDYwMzY0IDE2LjE2MzM0NTUsMTMuOTA2MDM2NCBDMTIuMzcyNDM2NCwxMy45MDYwMzY0IDkuMjk5MzQ1NDUsMTIuNzE0NzYzNiA5LjI5OTM0NTQ1LDExLjI0NTMwOTEgQzkuMjk5MzQ1NDUsOS43NzYwNzI3MyAxMi4zNzI0MzY0LDguNTg0OCAxNi4xNjMzNDU1LDguNTg0OCIgaWQ9IlNoYXBlIiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwb2x5bGluZSBpZD0iU2hhcGUiIGZpbGw9IiM2MjFCMUMiIHBvaW50cz0iNDAuNDI4IDcuODU0MTA5MDkgNDguMDI4NTgxOCAxMC44NTc2IDQwLjQzNDU0NTUgMTMuODU4MjU0NSA0MC40MjggNy44NTM4OTA5MSI+PC9wb2x5bGluZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHBvbHlsaW5lIGlkPSJTaGFwZSIgZmlsbD0iIzlBMjkyOCIgcG9pbnRzPSIzMi4wMTkyNzI3IDExLjE4MDI5MDkgNDAuNDI4IDcuODU0MTA5MDkgNDAuNDM0NTQ1NSAxMy44NTgyNTQ1IDM5LjYxMDAzNjQgMTQuMTgwNzI3MyAzMi4wMTkyNzI3IDExLjE4MDI5MDkiPjwvcG9seWxpbmU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4= mediatype: image/svg+xml - customresourcedefinitions: - owned: - - name: redisfailovers.databases.spotahome.com - displayName: RedisFailover - description: "Create a Redis sentinel instance" - kind: RedisFailover - version: v1 - specDescriptors: - - path: auth.secretPath - displayName: Secret Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:description:en:Input the secret name which includes the `password` key. If not exists, it will be generated with a random password' - - 'urn:alm:descriptor:description:zh:填写已有 Secret 名称,需包含`password`字段。如此 Secret 不存在,则会创建并生成随机密码。' - - path: redis.image - displayName: Redis Image - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Image' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_6_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_6_2_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_7_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_7_2_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.default:__DEFAULT_REDIS_IMAGE__' - - 'urn:alm:descriptor:description:en:Redis image' - - 'urn:alm:descriptor:description:zh:Redis 实例镜像' - - path: sentinel.image - displayName: Sentinel Image - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Image' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_6_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_6_2_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_7_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_7_2_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.default:__DEFAULT_REDIS_IMAGE__' - - 'urn:alm:descriptor:description:en:Sentinel image' - - 'urn:alm:descriptor:description:zh:Sentinel 实例镜像(请与Redis镜像版本保持一致)' - - path: redis.imagePullPolicy - displayName: Redis Image Pull Policy - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Image' - - 'urn:alm:descriptor:com.tectonic.ui:select:Always' - - 'urn:alm:descriptor:com.tectonic.ui:select:Never' - - 'urn:alm:descriptor:com.tectonic.ui:select:IfNotPresent' - - 'urn:alm:descriptor:com.tectonic.default:IfNotPresent' - - 'urn:alm:descriptor:description:en:Redis image pull policy' - - 'urn:alm:descriptor:description:zh:Redis 镜像拉取策略' - - path: sentinel.imagePullPolicy - displayName: Sentinel Image Pull Policy - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Image' - - 'urn:alm:descriptor:com.tectonic.ui:select:Always' - - 'urn:alm:descriptor:com.tectonic.ui:select:Never' - - 'urn:alm:descriptor:com.tectonic.ui:select:IfNotPresent' - - 'urn:alm:descriptor:com.tectonic.default:IfNotPresent' - - 'urn:alm:descriptor:description:en:Image pull policy' - - 'urn:alm:descriptor:description:zh:Sentinel 镜像拉取策略' - - path: redis.replicas - displayName: Redis Replicas - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:number' - - 'urn:alm:descriptor:com.tectonic.default:2' - - 'urn:alm:descriptor:description:en:Redis replicas' - - 'urn:alm:descriptor:description:zh:Redis 副本数' - - path: sentinel.replicas - displayName: Sentinel Replicas - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:number' - - 'urn:alm:descriptor:com.tectonic.default:3' - - 'urn:alm:descriptor:description:en:Sentinel replicas' - - 'urn:alm:descriptor:description:zh:Sentinel 副本数' - - path: enableTLS - displayName: Enable TLS - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:description:en:Enable TLS for redis >=6.0, client will need cert to connect' - - 'urn:alm:descriptor:description:zh:仅在 Redis >=6.0 版本可用,开启后,客户端连接 Redis 将需要使用证书。' - - path: redis.resources - displayName: Resource - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:resourceRequirements' - - path: redis.configConfigMap - displayName: Redis Config ConfigMap - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:com.tectonic.ui:advanced' - - 'urn:alm:descriptor:description:en:Redis config ConfigMap' - - 'urn:alm:descriptor:description:zh:Redis 配置文件所在配置字典名称,必须和实例在相同的命名空间,配置文件的 key 为 redis.conf,内容为配置文件的内容,支持注释。如果未填写,则会自动创建配置字典。' - - path: redis.storage.keepAfterDeletion - displayName: Keep After Deletion - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Storage' - - 'urn:alm:descriptor:description:en:Keep after deletion' - - 'urn:alm:descriptor:description:zh:删除实例后保留持久化卷' - - path: redis.storage.persistentVolumeClaim.metadata.name - displayName: PersistentVolumeClaim Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Storage' - - 'urn:alm:descriptor:label:en:Name' - - 'urn:alm:descriptor:label:zh:持久卷声明名称' - - 'urn:alm:descriptor:description:en:PersistentVolumeClaim Name' - - 'urn:alm:descriptor:description:zh:用于生成持久卷声明的名称,具有标识性即可。' - - path: redis.storage.persistentVolumeClaim.spec.storageClassName - displayName: StorageClass Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Storage' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:StorageClass Name' - - 'urn:alm:descriptor:label:zh:存储类名称' - - path: redis.storage.persistentVolumeClaim.spec.resources.requests.storage - displayName: Storage Size - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Storage' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Storage Size' - - 'urn:alm:descriptor:label:zh:存储大小' - - 'urn:alm:descriptor:description:en:Storage Size' - - 'urn:alm:descriptor:description:zh:需要带单位,如 10Gi。' - - path: redis.exporter.enabled - displayName: Enable Exporter - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Exporter' - - 'urn:alm:descriptor:com.tectonic.default:false' - - 'urn:alm:descriptor:description:en:Enable exporter' - - 'urn:alm:descriptor:description:zh:开启实例监控' - - path: redis.exporter.image - displayName: Exporter Image - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Exporter' - - 'urn:alm:descriptor:com.tectonic.ui:fieldDependency:redis.exporter.enabled:true' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:com.tectonic.default:__DEFAULT_EXPORTER_IMAGE__' - - 'urn:alm:descriptor:description:en:Exporter image' - - 'urn:alm:descriptor:description:zh:Redis 监控镜像' - - path: redis.exporter.imagePullPolicy - displayName: Redis Exporter Image Pull Policy - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Exporter' - - 'urn:alm:descriptor:com.tectonic.ui:fieldDependency:redis.exporter.enabled:true' - - 'urn:alm:descriptor:com.tectonic.ui:select:Always' - - 'urn:alm:descriptor:com.tectonic.ui:select:Never' - - 'urn:alm:descriptor:com.tectonic.ui:select:IfNotPresent' - - 'urn:alm:descriptor:com.tectonic.default:IfNotPresent' - - 'urn:alm:descriptor:description:en:Redis exporter image pull policy' - - 'urn:alm:descriptor:description:zh:Redis 监控镜像拉取策略' - - path: redis.backup.schedule[0].name - displayName: Schedule Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:description:en:Schedule name' - - 'urn:alm:descriptor:description:zh:定时备份名称' - - path: redis.backup.schedule[0].schedule - displayName: Schedule Policy - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:description:en:Schedule Policy, example: 0 0 * * *' - - 'urn:alm:descriptor:description:zh:备份时间策略,如: 0 0 * * *' - - path: redis.backup.schedule[0].keep - displayName: Keep Backup Last Number - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:number' - - 'urn:alm:descriptor:description:en:Keep backup last number' - - 'urn:alm:descriptor:description:zh:保留最近备份个数' - - path: redis.backup.schedule[0].keepAfterDeletion - displayName: Keep After Deletion - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:description:en:Keep after deletion' - - 'urn:alm:descriptor:description:zh:删除实例后是否保留存储卷' - - path: redis.backup.schedule[0].storage.storageClassName - displayName: StorageClass Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:description:en:StorageClass name' - - 'urn:alm:descriptor:description:zh:存储类名称' - - path: redis.backup.schedule[0].storage.size - displayName: Storage Size - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:description:en:Storage size, example: 10Gi' - - 'urn:alm:descriptor:description:zh:存储大小,如:10Gi' - - path: redis.restore.backupName - displayName: Backup Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Restore' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:description:en:Backup name' - - 'urn:alm:descriptor:description:zh:Redis 备份名称,将从该备份中恢复数据。' - - name: redisbackups.redis.middleware.alauda.io - displayName: RedisBackup - description: "Configure sentinel backups" - kind: RedisBackup - version: v1 - specDescriptors: - - path: source.redisFailoverName - displayName: Instance Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Instance Name' - - 'urn:alm:descriptor:label:zh:实例名称' - - 'urn:alm:descriptor:description:en:Instance Name' - - 'urn:alm:descriptor:description:zh:需要备份的 Redis 实例名称。' - - path: storage - displayName: Storage Size - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Storage Size' - - 'urn:alm:descriptor:label:zh:存储容量' - - 'urn:alm:descriptor:description:en:Storage Size' - - 'urn:alm:descriptor:description:zh: 备份存储大小,使用 Kubernetes 存储格式,如 10Gi' - - path: source.storageClassName - displayName: StorageClass Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:StorageClass Name' - - 'urn:alm:descriptor:label:zh:存储类名称' - - 'urn:alm:descriptor:description:en:StorageClass Name' - - 'urn:alm:descriptor:description:zh:备份所使用的存储类名称,只支持网络存储(如ceph-fs、NFS),不支持块存储(如local-path、topolvm、ceph-rbd)。' - - path: image - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - name: redisclusterbackups.redis.middleware.alauda.io - displayName: RedisBackup - description: "Configure cluster backups" - kind: RedisClusterBackup - version: v1 - specDescriptors: - - path: source.redisClusterName - displayName: Instance Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Instance Name' - - 'urn:alm:descriptor:label:zh:实例名称' - - 'urn:alm:descriptor:description:en:Instance Name' - - 'urn:alm:descriptor:description:zh:需要备份的 Redis 实例名称。' - - path: storage - displayName: Storage Size - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Storage Size' - - 'urn:alm:descriptor:label:zh:存储容量' - - 'urn:alm:descriptor:description:en:Storage Size' - - 'urn:alm:descriptor:description:zh: 备份存储大小,使用 Kubernetes 存储格式,如 10Gi' - - path: source.storageClassName - displayName: StorageClass Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:StorageClass Name' - - 'urn:alm:descriptor:label:zh:存储类名称' - - 'urn:alm:descriptor:description:en:StorageClass Name' - - 'urn:alm:descriptor:description:zh:备份所使用的存储类名称,只支持网络存储(如ceph-fs、NFS),不支持块存储(如local-path、topolvm、ceph-rbd)。' - - path: image - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - name: distributedredisclusters.redis.kun - displayName: DistributedRedisCluster - description: "Create a Redis cluster instance" - kind: DistributedRedisCluster - version: v1alpha1 - specDescriptors: - - path: image - displayName: Image - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_6_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_6_2_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_7_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.ui:select:__REDIS_VERSION_7_2_IMAGE__' - - 'urn:alm:descriptor:com.tectonic.default:__DEFAULT_REDIS_IMAGE__' - - 'urn:alm:descriptor:label:en:Image' - - 'urn:alm:descriptor:label:zh:实例镜像' - - 'urn:alm:descriptor:description:en:Image' - - 'urn:alm:descriptor:description:zh:实例镜像' - - path: imagePullPolicy - displayName: Image Pull Policy - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:select:Always' - - 'urn:alm:descriptor:com.tectonic.ui:select:Never' - - 'urn:alm:descriptor:com.tectonic.ui:select:IfNotPresent' - - 'urn:alm:descriptor:com.tectonic.default:IfNotPresent' - - 'urn:alm:descriptor:label:en:Image pull policy' - - 'urn:alm:descriptor:label:zh:镜像拉取策略' - - 'urn:alm:descriptor:description:en:Image pull policy' - - 'urn:alm:descriptor:description:zh:镜像拉取策略' - - path: masterSize - displayName: Master Size - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:number' - - 'urn:alm:descriptor:com.tectonic.default:3' - - 'urn:alm:descriptor:label:en:Master Size' - - 'urn:alm:descriptor:label:zh:分片数' - - 'urn:alm:descriptor:description:en:Master size' - - 'urn:alm:descriptor:description:zh:Redis 集群主节点数目。' - - path: clusterReplicas - displayName: Cluster Replicas - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:number' - - 'urn:alm:descriptor:com.tectonic.default:1' - - 'urn:alm:descriptor:label:en:Replicas per master' - - 'urn:alm:descriptor:label:zh:每分片从节点数' - - 'urn:alm:descriptor:description:en:Cluster replicas' - - 'urn:alm:descriptor:description:zh:Redis 集群每个主节点包含从节点个数。' - - path: resources - displayName: Resource - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:resourceRequirements' - - 'urn:alm:descriptor:label:en:Resource' - - 'urn:alm:descriptor:label:zh:资源配额' - - path: enableTLS - displayName: Enable TLS - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:label:en:Enable TLS' - - 'urn:alm:descriptor:label:zh:开启 TLS 加密' - - 'urn:alm:descriptor:description:en:Enable TLS for redis >=6.0, client will need cert to connect' - - 'urn:alm:descriptor:description:zh:仅在 Redis >=6.0 版本可用,开启后,客户端连接 Redis 将需要使用证书。' - - path: requiredAntiAffinity - displayName: Required Anti Affinity - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:label:en:Anti Affinity' - - 'urn:alm:descriptor:label:zh:开启反亲和' - - 'urn:alm:descriptor:description:en:Required anti affinity' - - 'urn:alm:descriptor:description:zh:强制 master 和 slave 部署在不同节点。' - - path: storage.deleteClaim - displayName: Delete Claim - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:storage' - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:label:en:Delete Claim' - - 'urn:alm:descriptor:label:zh:是否删除持久卷' - - 'urn:alm:descriptor:description:en:Delete Claim' - - 'urn:alm:descriptor:description:zh:删除实例时是否删除持久卷。' - - path: storage.class - displayName: StorageClass Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:storage' - - 'urn:alm:descriptor:label:en:StorageClass Name' - - 'urn:alm:descriptor:label:zh:存储类名称' - - path: storage.size - displayName: Size - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:storage' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Size' - - 'urn:alm:descriptor:label:zh:存储容量' - - 'urn:alm:descriptor:description:en:Storage Size' - - 'urn:alm:descriptor:description:zh:声明持久化存储容量,需要带单位,如 10Gi。' - - path: storage.type - displayName: Storage Type - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:storage' - - 'urn:alm:descriptor:com.tectonic.ui:select:persistent-claim' - - 'urn:alm:descriptor:com.tectonic.ui:select:ephemeral' - - 'urn:alm:descriptor:com.tectonic.default:persistent-claim' - - 'urn:alm:descriptor:label:en:Storage Type' - - 'urn:alm:descriptor:label:zh:存储类型' - - 'urn:alm:descriptor:description:en:Storage Type' - - 'urn:alm:descriptor:description:zh:存储类型,persistent-claim(持久卷类型)和 ephemeral(临时存储)。' - - path: passwordSecret.name - displayName: Secret Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Secret Name' - - 'urn:alm:descriptor:label:zh:Secret名称' - - 'urn:alm:descriptor:description:en:Input the secret name which includes the `password` key. If not exists, it will be generated with a random password' - - 'urn:alm:descriptor:description:zh:填写已有 Secret 名称,需包含`password`字段。如此 Secret 不存在,则会创建并生成随机密码。' - - path: backup.schedule[0].name - displayName: Schedule Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Schedule Name' - - 'urn:alm:descriptor:label:zh:定时备份名称' - - 'urn:alm:descriptor:description:en:Schedule name' - - 'urn:alm:descriptor:description:zh:定时备份名称,用于标识定时备份,名称需唯一。' - - path: backup.schedule[0].schedule - displayName: Schedule Policy - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Schedule Policy' - - 'urn:alm:descriptor:label:zh:定时策略' - - 'urn:alm:descriptor:description:en:Schedule Policy, example: 0 0 * * *' - - 'urn:alm:descriptor:description:zh:备份时间策略,如: 0 0 * * *' - - path: backup.schedule[0].keep - displayName: Keep Backup Last Number - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:number' - - 'urn:alm:descriptor:label:en:Keep Backup Last Number' - - 'urn:alm:descriptor:label:zh:保留个数' - - 'urn:alm:descriptor:description:en:Keep backup last number' - - 'urn:alm:descriptor:description:zh:保留最近备份个数' - - path: backup.schedule[0].keepAfterDeletion - displayName: Keep After Deletion - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' - - 'urn:alm:descriptor:label:en:Keep After Deletion' - - 'urn:alm:descriptor:label:zh:保留持久卷' - - 'urn:alm:descriptor:com.tectonic.default:true' - - 'urn:alm:descriptor:description:en:Keep after deletion' - - 'urn:alm:descriptor:description:zh:删除实例后是否保留存储卷' - - path: backup.schedule[0].storage.storageClassName - displayName: StorageClass Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:StorageClass Name' - - 'urn:alm:descriptor:label:zh:存储类名称' - - 'urn:alm:descriptor:description:en:StorageClass name' - - 'urn:alm:descriptor:description:zh:存储类名称' - - path: backup.schedule[0].storage.size - displayName: Storage Size - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Backup' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Storage Size' - - 'urn:alm:descriptor:label:zh:存储容量' - - 'urn:alm:descriptor:description:en:Storage size, example: 10Gi' - - 'urn:alm:descriptor:description:zh:存储大小,如:10Gi' - - path: restore.backupName - displayName: Backup Name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:fieldGroup:Restore' - - 'urn:alm:descriptor:com.tectonic.ui:text' - - 'urn:alm:descriptor:label:en:Backup Name' - - 'urn:alm:descriptor:label:zh:备份名称' - - 'urn:alm:descriptor:description:en:RedisClusterBackup name' - - 'urn:alm:descriptor:description:zh:Redis 备份名称,将从该备份中恢复数据。' - - path: restore.imagePullPolicy - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: restore.image - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: serviceName - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: imagePullSecrets[0].name - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: toleRations[0].operator - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: toleRations[0].key - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: toleRations[0].value - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: toleRations[0].effect - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' - - path: toleRations[0].tolerationSeconds - x-descriptors: - - 'urn:alm:descriptor:com.tectonic.ui:hidden' install: strategy: deployment spec: deployments: null webhookdefinitions: - admissionReviewVersions: + - v1beta1 - v1 type: ValidatingAdmissionWebhook containerPort: 443 @@ -545,3 +81,12 @@ spec: supported: false - type: AllNamespaces supported: true + relatedImages: + - name: redis-exporter + image: oliver006/redis_exporter:v1.3.5 + - name: redis-v5 + image: redis:5.0-alpine + - name: redis-v6 + image: redis:6.0-alpine + - name: redis-v72 + image: redis:7.2-alpine diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml index 333071a..b903146 100644 --- a/config/manifests/kustomization.yaml +++ b/config/manifests/kustomization.yaml @@ -6,3 +6,5 @@ resources: - ../samples - ../scorecard - ../webhook +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/config/rbac/databases_redissentinel_editor_role.yaml b/config/rbac/databases_redissentinel_editor_role.yaml new file mode 100644 index 0000000..cb611f1 --- /dev/null +++ b/config/rbac/databases_redissentinel_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit redissentinels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: redis-operator + app.kubernetes.io/managed-by: kustomize + name: databases-redissentinel-editor-role +rules: +- apiGroups: + - databases + resources: + - redissentinels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - databases + resources: + - redissentinels/status + verbs: + - get diff --git a/config/rbac/databases_redissentinel_viewer_role.yaml b/config/rbac/databases_redissentinel_viewer_role.yaml new file mode 100644 index 0000000..54f706b --- /dev/null +++ b/config/rbac/databases_redissentinel_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view redissentinels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: redis-operator + app.kubernetes.io/managed-by: kustomize + name: databases-redissentinel-viewer-role +rules: +- apiGroups: + - databases + resources: + - redissentinels + verbs: + - get + - list + - watch +- apiGroups: + - databases + resources: + - redissentinels/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 29a0c8b..939ae62 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -3,3 +3,7 @@ resources: - role.yaml - role_binding.yaml - leader_election_role.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. diff --git a/config/rbac/middleware_redis_editor_role.yaml b/config/rbac/middleware_redis_editor_role.yaml new file mode 100644 index 0000000..2eb4df0 --- /dev/null +++ b/config/rbac/middleware_redis_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit redis. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: redis-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: redis-operator + app.kubernetes.io/part-of: redis-operator + app.kubernetes.io/managed-by: kustomize + name: redis-editor-role +rules: +- apiGroups: + - middleware + resources: + - redis + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - middleware + resources: + - redis/status + verbs: + - get diff --git a/config/rbac/middleware_redis_viewer_role.yaml b/config/rbac/middleware_redis_viewer_role.yaml new file mode 100644 index 0000000..3aca83b --- /dev/null +++ b/config/rbac/middleware_redis_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view redis. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: redis-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: redis-operator + app.kubernetes.io/part-of: redis-operator + app.kubernetes.io/managed-by: kustomize + name: redis-viewer-role +rules: +- apiGroups: + - middleware + resources: + - redis + verbs: + - get + - list + - watch +- apiGroups: + - middleware + resources: + - redis/status + verbs: + - get diff --git a/config/rbac/redis.middleware.alauda.io_redisbackup_editor_role.yaml b/config/rbac/redis.middleware.alauda.io_redisbackup_editor_role.yaml deleted file mode 100644 index 0ea19d6..0000000 --- a/config/rbac/redis.middleware.alauda.io_redisbackup_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit redisbackups. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: redisbackup-editor-role -rules: -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisbackups - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisbackups/status - verbs: - - get diff --git a/config/rbac/redis.middleware.alauda.io_redisbackup_viewer_role.yaml b/config/rbac/redis.middleware.alauda.io_redisbackup_viewer_role.yaml deleted file mode 100644 index ef9610b..0000000 --- a/config/rbac/redis.middleware.alauda.io_redisbackup_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view redisbackups. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: redisbackup-viewer-role -rules: -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisbackups - verbs: - - get - - list - - watch -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisbackups/status - verbs: - - get diff --git a/config/rbac/redis.middleware.alauda.io_redisclusterbackup_editor_role.yaml b/config/rbac/redis.middleware.alauda.io_redisclusterbackup_editor_role.yaml deleted file mode 100644 index 33a1d85..0000000 --- a/config/rbac/redis.middleware.alauda.io_redisclusterbackup_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit redisclusterbackups. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: redisclusterbackup-editor-role -rules: -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisclusterbackups - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisclusterbackups/status - verbs: - - get diff --git a/config/rbac/redis.middleware.alauda.io_redisclusterbackup_viewer_role.yaml b/config/rbac/redis.middleware.alauda.io_redisclusterbackup_viewer_role.yaml deleted file mode 100644 index b09ca35..0000000 --- a/config/rbac/redis.middleware.alauda.io_redisclusterbackup_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view redisclusterbackups. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: redisclusterbackup-viewer-role -rules: -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisclusterbackups - verbs: - - get - - list - - watch -- apiGroups: - - redis.middleware.alauda.io - resources: - - redisclusterbackups/status - verbs: - - get diff --git a/config/rbac/redisbackup_editor_role.yaml b/config/rbac/redisbackup_editor_role.yaml deleted file mode 100644 index e540509..0000000 --- a/config/rbac/redisbackup_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit redisbackups. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: redisbackup-editor-role -rules: -- apiGroups: - - middle.alauda.cn - resources: - - redisbackups - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - middle.alauda.cn - resources: - - redisbackups/status - verbs: - - get diff --git a/config/rbac/redisbackup_viewer_role.yaml b/config/rbac/redisbackup_viewer_role.yaml deleted file mode 100644 index a47dad4..0000000 --- a/config/rbac/redisbackup_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view redisbackups. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: redisbackup-viewer-role -rules: -- apiGroups: - - middle.alauda.cn - resources: - - redisbackups - verbs: - - get - - list - - watch -- apiGroups: - - middle.alauda.cn - resources: - - redisbackups/status - verbs: - - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index cb1c8cf..cd7594c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -8,17 +8,13 @@ rules: - '*' resources: - configmaps - - configmaps/finalizers - endpoints - - persistentvolumeclaims - - persistentvolumeclaims/finalizers - pods - pods/exec - - secrets - - secrets/finalizers - services - services/finalizers verbs: + - '*' - create - delete - deletecollection @@ -30,14 +26,20 @@ rules: - apiGroups: - '*' resources: - - configmaps - - endpoints - - pods - - pods/exec - - services - - services/finalizers + - configmaps/finalizers + - persistentvolumeclaims + - persistentvolumeclaims/finalizers + - secrets + - secrets/finalizers verbs: - - '*' + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch - apiGroups: - '*' resources: @@ -138,16 +140,10 @@ rules: - update - watch - apiGroups: - - middleware.alauda.io - resources: - - redis - verbs: - - '*' -- apiGroups: - - policy + - databases.spotahome.com resources: - - poddisruptionbudgets - - poddisruptionbudgets/finalizers + - redisfailovers + - redissentinels verbs: - create - delete @@ -157,12 +153,25 @@ rules: - update - watch - apiGroups: - - rbac.authorization.k8s.io + - databases.spotahome.com resources: - - clusterrolebindings - - clusterroles - - rolebindings - - roles + - redisfailovers/finalizers + - redissentinels/finalizers + verbs: + - update +- apiGroups: + - databases.spotahome.com + resources: + - redisfailovers/status + - redissentinels/status + verbs: + - get + - patch + - update +- apiGroups: + - middleware.alauda.io + resources: + - imageversions verbs: - create - delete @@ -172,16 +181,11 @@ rules: - update - watch - apiGroups: - - redis.kun + - middleware.alauda.io resources: - - '*' + - redis verbs: - '*' -- apiGroups: - - redis.kun - resources: - - distributedredisclusters - verbs: - create - delete - get @@ -190,29 +194,24 @@ rules: - update - watch - apiGroups: - - redis.kun + - middleware.alauda.io resources: - - distributedredisclusters/finalizers + - redis/finalizers verbs: - update - apiGroups: - - redis.kun + - middleware.alauda.io resources: - - distributedredisclusters/status + - redis/status verbs: - get - patch - update - apiGroups: - - redis.middleware.alauda.io - resources: - - '*' - verbs: - - '*' -- apiGroups: - - redis.middleware.alauda.io + - policy resources: - - redisbackups + - poddisruptionbudgets + - poddisruptionbudgets/finalizers verbs: - create - delete @@ -222,23 +221,24 @@ rules: - update - watch - apiGroups: - - redis.middleware.alauda.io - resources: - - redisbackups/finalizers - verbs: - - update -- apiGroups: - - redis.middleware.alauda.io + - rbac.authorization.k8s.io resources: - - redisbackups/status + - clusterrolebindings + - clusterroles + - rolebindings + - roles verbs: + - create + - delete - get + - list - patch - update + - watch - apiGroups: - - redis.middleware.alauda.io + - redis.kun resources: - - redisclusterbackups + - distributedredisclusters verbs: - create - delete @@ -248,19 +248,26 @@ rules: - update - watch - apiGroups: - - redis.middleware.alauda.io + - redis.kun resources: - - redisclusterbackups/finalizers + - distributedredisclusters/finalizers verbs: - update - apiGroups: - - redis.middleware.alauda.io + - redis.kun resources: - - redisclusterbackups/status + - distributedredisclusters/status verbs: - get - patch - update +- apiGroups: + - redis.kun + - redis.middleware.alauda.io + resources: + - '*' + verbs: + - '*' - apiGroups: - redis.middleware.alauda.io resources: diff --git a/config/samples/backup/backup.yaml b/config/samples/backup/backup.yaml new file mode 100644 index 0000000..c961f71 --- /dev/null +++ b/config/samples/backup/backup.yaml @@ -0,0 +1,9 @@ +apiVersion: redis.middleware.alauda.io/v1 +kind: RedisBackup +metadata: + name: redis-backup-demo +spec: + source: + redisFailoverName: redis-sentinel-demo + storageClassName: '' + storage: 1Gi \ No newline at end of file diff --git a/config/samples/cluster/backup.yaml b/config/samples/cluster/backup.yaml new file mode 100644 index 0000000..ed2f01b --- /dev/null +++ b/config/samples/cluster/backup.yaml @@ -0,0 +1,9 @@ +apiVersion: redis.middleware.alauda.io/v1 +kind: RedisClusterBackup +metadata: + name: redis-cluster-backup-demo +spec: + source: + redisClusterName: redis-cluster-demo + storageClassName: '' + storage: 1Gi \ No newline at end of file diff --git a/config/samples/cluster/cluster.yaml b/config/samples/cluster/cluster.yaml new file mode 100644 index 0000000..aa58a29 --- /dev/null +++ b/config/samples/cluster/cluster.yaml @@ -0,0 +1,10 @@ +apiVersion: redis.kun/v1alpha1 +kind: DistributedRedisCluster +metadata: + name: redis-cluster-demo +spec: + masterSize: 3 + clusterReplicas: 1 + image: __DEFAULT_REDIS_IMAGE__ + monitor: + image: __DEFAULT_EXPORTER_IMAGE__ diff --git a/config/samples/databases.spotahome.com_v1_redisfailover.yaml b/config/samples/databases.spotahome.com_v1_redisfailover.yaml deleted file mode 100644 index 5aca709..0000000 --- a/config/samples/databases.spotahome.com_v1_redisfailover.yaml +++ /dev/null @@ -1,160 +0,0 @@ -apiVersion: databases.spotahome.com/v1 -kind: RedisFailover -metadata: - name: redissentinel -spec: - auth: - secretPath: redis-redissentinel-6fp8p - expose: - enableNodePort: true - redis: - affinity: - podAntiAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: app.kubernetes.io/component - operator: In - values: - - redis - - key: redisfailovers.databases.spotahome.com/name - operator: In - values: - - redissentinel - topologyKey: kubernetes.io/hostname - customConfig: - acllog-max-len: "128" - active-defrag-cycle-max: "25" - active-defrag-cycle-min: "1" - active-defrag-ignore-bytes: 100mb - active-defrag-max-scan-fields: "1000" - active-defrag-threshold-lower: "10" - active-defrag-threshold-upper: "100" - active-expire-effort: "1" - activedefrag: "no" - activerehashing: "yes" - aof-load-truncated: "yes" - aof-rewrite-incremental-fsync: "yes" - aof-use-rdb-preamble: "yes" - appendfsync: everysec - appendonly: "no" - auto-aof-rewrite-min-size: 64mb - auto-aof-rewrite-percentage: "100" - client-output-buffer-limit: normal 0 0 0 slave 268435456 67108864 60 pubsub - 33554432 8388608 60 - client-query-buffer-limit: 1gb - databases: "16" - dynamic-hz: "yes" - hash-max-ziplist-entries: "512" - hash-max-ziplist-value: "64" - hll-sparse-max-bytes: "3000" - hz: "10" - io-threads: "4" - io-threads-do-reads: "no" - jemalloc-bg-thread: "yes" - latency-monitor-threshold: "0" - lazyfree-lazy-eviction: "no" - lazyfree-lazy-expire: "no" - lazyfree-lazy-server-del: "no" - lazyfree-lazy-user-del: "no" - lfu-decay-time: "1" - lfu-log-factor: "10" - list-compress-depth: "0" - list-max-ziplist-size: "-2" - loglevel: notice - lua-time-limit: "5000" - maxclients: "10000" - maxmemory-policy: noeviction - maxmemory-samples: "5" - min-replicas-max-lag: "10" - min-replicas-to-write: "0" - no-appendfsync-on-rewrite: "no" - oom-score-adj: "no" - oom-score-adj-values: 0 200 800 - proto-max-bulk-len: 512mb - rdb-save-incremental-fsync: "yes" - rdbchecksum: "yes" - rdbcompression: "yes" - repl-backlog-size: "21474836" - repl-backlog-ttl: "3600" - repl-disable-tcp-nodelay: "no" - repl-diskless-load: disabled - repl-diskless-sync: "no" - repl-diskless-sync-delay: "5" - repl-ping-replica-period: "10" - repl-timeout: "60" - replica-ignore-maxmemory: "yes" - replica-lazy-flush: "no" - replica-serve-stale-data: "yes" - save: 60 10000 300 100 600 1 - set-max-intset-entries: "512" - slowlog-log-slower-than: "10000" - slowlog-max-len: "128" - stop-writes-on-bgsave-error: "yes" - stream-node-max-bytes: "4096" - stream-node-max-entries: "100" - tcp-backlog: "511" - tcp-keepalive: "300" - timeout: "0" - tracking-table-max-keys: "1000000" - zset-max-ziplist-entries: "128" - zset-max-ziplist-value: "64" - exporter: - enabled: true - image: oliver006/redis_exporter:v1.55.0 - image: redis:6.0-alpine - replicas: 2 - resources: - limits: - cpu: "1" - memory: 2Gi - requests: - cpu: "1" - memory: 2Gi - storage: - keepAfterDeletion: true - persistentVolumeClaim: - metadata: - labels: - app.kubernetes.io/name: redissentinel - app.kubernetes.io/part-of: redis-failover - middleware.instance/name: redissentinel - middleware.instance/type: redis-failover - name: redis-data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi - storageClassName: sc-topolvm - volumeMode: Filesystem - status: {} - sentinel: - affinity: - podAntiAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: app.kubernetes.io/component - operator: In - values: - - sentinel - - key: redisfailovers.databases.spotahome.com/name - operator: In - values: - - redissentinel - topologyKey: kubernetes.io/hostname - customConfig: - down-after-milliseconds: "30000" - failover-timeout: "180000" - parallel-syncs: "1" - image: redis:6.0-alpine - replicas: 3 - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi diff --git a/config/samples/databases_v1_redissentinel.yaml b/config/samples/databases_v1_redissentinel.yaml new file mode 100644 index 0000000..dd9f530 --- /dev/null +++ b/config/samples/databases_v1_redissentinel.yaml @@ -0,0 +1,9 @@ +apiVersion: databases/v1 +kind: RedisSentinel +metadata: + labels: + app.kubernetes.io/name: redis-operator + app.kubernetes.io/managed-by: kustomize + name: redissentinel-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index b3d7964..6151e97 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,7 +1,8 @@ resources: - - databases.spotahome.com_v1_redisfailover.yaml - - redis.kun_v1alpha1_distributedrediscluster.yaml - - redis.middleware.alauda.io_v1_redisbackup.yaml - - redis.middleware.alauda.io_v1_redisclusterbackup.yaml + - cluster/cluster.yaml + - cluster/backup.yaml + - backup/backup.yaml + - sentinel/sentinel.yaml + - proxy/proxy.yaml + - shake/redisshake.yaml - redis.middleware.alauda.io_v1_redisuser.yaml - diff --git a/config/samples/middleware_v1_redis.yaml b/config/samples/middleware_v1_redis.yaml new file mode 100644 index 0000000..ac8d386 --- /dev/null +++ b/config/samples/middleware_v1_redis.yaml @@ -0,0 +1,12 @@ +apiVersion: middleware/v1 +kind: Redis +metadata: + labels: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: redis-sample + app.kubernetes.io/part-of: redis-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: redis-operator + name: redis-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/proxy/proxy.yaml b/config/samples/proxy/proxy.yaml new file mode 100644 index 0000000..2a3ae01 --- /dev/null +++ b/config/samples/proxy/proxy.yaml @@ -0,0 +1,9 @@ +apiVersion: middle.alauda.cn/v1alpha1 +kind: RedisProxy +metadata: + name: redisproxy-sample +spec: + proxyInfo: + architecture: cluster + instanceName: redisproxy-sample + image: __DEFAULT_PROXY_IMAGE__ diff --git a/config/samples/redis.kun_v1alpha1_distributedrediscluster.yaml b/config/samples/redis.kun_v1alpha1_distributedrediscluster.yaml index a6556f3..247a37f 100644 --- a/config/samples/redis.kun_v1alpha1_distributedrediscluster.yaml +++ b/config/samples/redis.kun_v1alpha1_distributedrediscluster.yaml @@ -1,114 +1,6 @@ apiVersion: redis.kun/v1alpha1 kind: DistributedRedisCluster metadata: - name: rediscluster + name: distributedrediscluster-sample spec: - affinityPolicy: AntiAffinityInSharding - clusterReplicas: 1 - config: - acllog-max-len: "128" - active-defrag-cycle-max: "25" - active-defrag-cycle-min: "1" - active-defrag-ignore-bytes: 100mb - active-defrag-max-scan-fields: "1000" - active-defrag-threshold-lower: "10" - active-defrag-threshold-upper: "100" - active-expire-effort: "1" - activedefrag: "no" - activerehashing: "yes" - aof-load-truncated: "yes" - aof-rewrite-incremental-fsync: "yes" - aof-use-rdb-preamble: "yes" - appendfsync: everysec - appendonly: "no" - auto-aof-rewrite-min-size: 64mb - auto-aof-rewrite-percentage: "100" - client-output-buffer-limit: normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 - 8388608 60 - client-query-buffer-limit: 1gb - cluster-migration-barrier: "10" - cluster-node-timeout: "15000" - cluster-replica-validity-factor: "10" - cluster-require-full-coverage: "yes" - databases: "16" - dynamic-hz: "yes" - hash-max-ziplist-entries: "512" - hash-max-ziplist-value: "64" - hll-sparse-max-bytes: "3000" - hz: "10" - io-threads: "4" - io-threads-do-reads: "no" - jemalloc-bg-thread: "yes" - latency-monitor-threshold: "0" - lazyfree-lazy-eviction: "no" - lazyfree-lazy-expire: "no" - lazyfree-lazy-server-del: "no" - lazyfree-lazy-user-del: "no" - lfu-decay-time: "1" - lfu-log-factor: "10" - list-compress-depth: "0" - list-max-ziplist-size: "-2" - loglevel: notice - lua-time-limit: "5000" - maxclients: "10000" - maxmemory-policy: noeviction - maxmemory-samples: "5" - min-replicas-max-lag: "10" - min-replicas-to-write: "0" - no-appendfsync-on-rewrite: "no" - oom-score-adj: "no" - oom-score-adj-values: 0 200 800 - proto-max-bulk-len: 512mb - rdb-save-incremental-fsync: "yes" - rdbchecksum: "yes" - rdbcompression: "yes" - repl-backlog-size: "21474836" - repl-backlog-ttl: "3600" - repl-disable-tcp-nodelay: "no" - repl-diskless-load: disabled - repl-diskless-sync: "no" - repl-diskless-sync-delay: "5" - repl-ping-replica-period: "10" - repl-timeout: "60" - replica-ignore-maxmemory: "yes" - replica-lazy-flush: "no" - replica-serve-stale-data: "yes" - save: 60 10000 300 100 600 1 - set-max-intset-entries: "512" - slowlog-log-slower-than: "10000" - slowlog-max-len: "128" - stop-writes-on-bgsave-error: "yes" - stream-node-max-bytes: "4096" - stream-node-max-entries: "100" - tcp-backlog: "511" - tcp-keepalive: "300" - timeout: "0" - tracking-table-max-keys: "1000000" - zset-max-ziplist-entries: "128" - zset-max-ziplist-value: "64" - expose: - enableNodePort: true - image: redis:6.0-alpine - masterSize: 3 - monitor: - image: oliver006/redis_exporter:v1.55.0 - resources: - limits: - cpu: 100m - memory: 300Mi - requests: - cpu: 100m - memory: 300Mi - passwordSecret: - name: redis-rediscluster-7j8xn - resources: - limits: - cpu: "1" - memory: 2Gi - requests: - cpu: "1" - memory: 2Gi - storage: - class: sc-topolvm - size: 1Gi - type: persistent-claim + # TODO(user): Add fields here diff --git a/config/samples/redis.middleware.alauda.io_v1_redisbackup.yaml b/config/samples/redis.middleware.alauda.io_v1_redisbackup.yaml deleted file mode 100644 index 343ca37..0000000 --- a/config/samples/redis.middleware.alauda.io_v1_redisbackup.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: redis.middleware.alauda.io/v1 -kind: RedisBackup -metadata: - labels: - redisfailovers.databases.spotahome.com/name: redissentinel - name: redissentinel-1 -spec: - resources: - limits: - cpu: 500m - memory: 1Gi - requests: - cpu: 500m - memory: 500Mi - source: - redisFailoverName: redissentinel - storageClassName: gp3 - storage: 1Gi diff --git a/config/samples/redis.middleware.alauda.io_v1_redisclusterbackup.yaml b/config/samples/redis.middleware.alauda.io_v1_redisclusterbackup.yaml index 8579fb2..66d9b4c 100644 --- a/config/samples/redis.middleware.alauda.io_v1_redisclusterbackup.yaml +++ b/config/samples/redis.middleware.alauda.io_v1_redisclusterbackup.yaml @@ -1,11 +1,6 @@ apiVersion: redis.middleware.alauda.io/v1 kind: RedisClusterBackup metadata: - labels: - redis.kun/name: rediscluster - name: rediscluster-1 + name: redisclusterbackup-sample spec: - source: - redisClusterName: rediscluster - storageClassName: gp3 - storage: 6Gi + # TODO(user): Add fields here diff --git a/config/samples/redis.middleware.alauda.io_v1_redisuser.yaml b/config/samples/redis.middleware.alauda.io_v1_redisuser.yaml index 2b4df36..aeb4b8d 100644 --- a/config/samples/redis.middleware.alauda.io_v1_redisuser.yaml +++ b/config/samples/redis.middleware.alauda.io_v1_redisuser.yaml @@ -1,12 +1,6 @@ apiVersion: redis.middleware.alauda.io/v1 kind: RedisUser metadata: - name: rfr-acl-redissentinel-reader + name: redisuser-sample spec: - accountType: custom - aclRules: +@all -flushall -flushdb -keys -acl - arch: sentinel - passwordSecrets: - - redis-reader-v1u864 - redisName: redissentinel - username: reader + # TODO(user): Add fields here diff --git a/config/samples/redis.middleware.alauda.io_v1alpha1_activeredis.yaml b/config/samples/redis.middleware.alauda.io_v1alpha1_activeredis.yaml new file mode 100644 index 0000000..e9b2b15 --- /dev/null +++ b/config/samples/redis.middleware.alauda.io_v1alpha1_activeredis.yaml @@ -0,0 +1,12 @@ +apiVersion: redis.middleware.alauda.io/v1alpha1 +kind: ActiveRedis +metadata: + labels: + app.kubernetes.io/name: activeredis + app.kubernetes.io/instance: activeredis-sample + app.kubernetes.io/part-of: redis-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: redis-operator + name: activeredis-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/redis.middleware.alauda.io_v1alpha1_activeredisconnection.yaml b/config/samples/redis.middleware.alauda.io_v1alpha1_activeredisconnection.yaml new file mode 100644 index 0000000..b0b2a0e --- /dev/null +++ b/config/samples/redis.middleware.alauda.io_v1alpha1_activeredisconnection.yaml @@ -0,0 +1,12 @@ +apiVersion: redis.middleware.alauda.io/v1alpha1 +kind: ActiveRedisConnection +metadata: + labels: + app.kubernetes.io/name: activeredisconnection + app.kubernetes.io/instance: activeredisconnection-sample + app.kubernetes.io/part-of: redis-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: redis-operator + name: activeredisconnection-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/sentinel/sentinel.yaml b/config/samples/sentinel/sentinel.yaml new file mode 100644 index 0000000..91fb8b4 --- /dev/null +++ b/config/samples/sentinel/sentinel.yaml @@ -0,0 +1,5 @@ +apiVersion: databases.spotahome.com/v1 +kind: RedisFailover +metadata: + name: redis-sentinel-demo +spec: {} \ No newline at end of file diff --git a/config/samples/shake/redisshake.yaml b/config/samples/shake/redisshake.yaml new file mode 100644 index 0000000..5b1291b --- /dev/null +++ b/config/samples/shake/redisshake.yaml @@ -0,0 +1,20 @@ +apiVersion: middle.alauda.cn/v1alpha1 +kind: RedisShake +metadata: + name: redisshake-sample +spec: + # Add fields here + modelType: sync + keyExists: rewrite + image: __DEFAULT_SHAKE_IMAGE__ + resumeFromBreakPoint: true + source: + type: sentinel + clusterName: redis-sentinel-demo + target: + type: sentinel + address: + - mymaster@127.0.0.1:26379 + + + diff --git a/config/scorecard/kustomization.yaml b/config/scorecard/kustomization.yaml index 61ceb4d..50cd2d0 100644 --- a/config/scorecard/kustomization.yaml +++ b/config/scorecard/kustomization.yaml @@ -1,17 +1,16 @@ resources: - bases/config.yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -patches: +patchesJson6902: - path: patches/basic.config.yaml target: group: scorecard.operatorframework.io + version: v1alpha3 kind: Configuration name: config - version: v1alpha3 - path: patches/olm.config.yaml target: group: scorecard.operatorframework.io + version: v1alpha3 kind: Configuration name: config - version: v1alpha3 +#+kubebuilder:scaffold:patchesJson6902 diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index c6cfb3d..f6997f4 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -5,7 +5,7 @@ metadata: name: mutating-webhook-configuration webhooks: - admissionReviewVersions: - - v1beta1 + - v1 clientConfig: service: name: webhook-service @@ -24,6 +24,26 @@ webhooks: resources: - redisusers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-middleware-alauda-io-v1-redis + failurePolicy: Fail + name: mredis.kb.io + rules: + - apiGroups: + - middleware.alauda.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - redis + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -31,7 +51,7 @@ metadata: name: validating-webhook-configuration webhooks: - admissionReviewVersions: - - v1beta1 + - v1 clientConfig: service: name: webhook-service @@ -51,3 +71,24 @@ webhooks: resources: - redisusers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-middleware-alauda-io-v1-redis + failurePolicy: Fail + name: vredis.kb.io + rules: + - apiGroups: + - middleware.alauda.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - redis + sideEffects: None diff --git a/go.mod b/go.mod index 93a2371..d5840e9 100644 --- a/go.mod +++ b/go.mod @@ -1,95 +1,86 @@ module github.com/alauda/redis-operator -go 1.20 +go 1.22 require ( github.com/Masterminds/semver/v3 v3.1.1 github.com/cert-manager/cert-manager v1.9.1 - github.com/fsnotify/fsnotify v1.7.0 - github.com/go-logr/logr v1.4.1 + github.com/fsnotify/fsnotify v1.6.0 + github.com/go-logr/logr v1.2.4 github.com/gomodule/redigo v1.8.9 - github.com/minio/minio-go/v7 v7.0.49 - github.com/onsi/ginkgo v1.16.5 - github.com/onsi/ginkgo/v2 v2.9.5 - github.com/onsi/gomega v1.27.7 + github.com/onsi/ginkgo/v2 v2.11.0 + github.com/onsi/gomega v1.27.10 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.57.0 - github.com/urfave/cli/v2 v2.27.1 - go.uber.org/zap v1.24.0 - k8s.io/api v0.27.7 - k8s.io/apimachinery v0.27.7 - k8s.io/client-go v0.27.7 - k8s.io/utils v0.0.0-20230209194617-a36077c30491 - sigs.k8s.io/controller-runtime v0.15.3 + github.com/samber/lo v1.46.0 + github.com/stretchr/testify v1.9.0 + github.com/urfave/cli/v2 v2.27.4 + go.uber.org/zap v1.25.0 + gotest.tools/v3 v3.0.3 + k8s.io/api v0.28.11 + k8s.io/apimachinery v0.28.11 + k8s.io/client-go v0.28.11 + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 + sigs.k8s.io/controller-runtime v0.16.5 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.15 // indirect - github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nxadm/tail v1.4.8 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.15.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect - github.com/rs/xid v1.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.9.1 // indirect - gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.27.7 // indirect - k8s.io/component-base v0.27.7 // indirect - k8s.io/klog/v2 v2.90.1 // indirect - k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect + k8s.io/apiextensions-apiserver v0.28.3 // indirect + k8s.io/component-base v0.28.3 // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect sigs.k8s.io/gateway-api v0.4.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect diff --git a/go.sum b/go.sum index 8905f5c..ae0b198 100644 --- a/go.sum +++ b/go.sum @@ -68,9 +68,11 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -79,7 +81,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cert-manager/cert-manager v1.9.1 h1:bNIsQyfWdIMSEwxgO4sVUEyAn6xuSgNwdt9m92OBACc= github.com/cert-manager/cert-manager v1.9.1/go.mod h1:Bs3WsNX1LPKTs3boh//p7jLOn6ZRGEPz99ITeZU0g3c= @@ -94,7 +95,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -107,10 +107,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -122,20 +120,17 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -146,15 +141,12 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -171,8 +163,6 @@ github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= @@ -184,8 +174,8 @@ github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaL github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= -github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= @@ -236,15 +226,15 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -257,8 +247,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -283,8 +273,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= @@ -347,12 +337,6 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= -github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= -github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -361,6 +345,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -386,12 +371,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.49 h1:dE5DfOtnXMXCjr/HWI6zN9vCrY6Sv666qhhiwUMvGV4= -github.com/minio/minio-go/v7 v7.0.49/go.mod h1:UI34MvQEiob3Cf/gGExGMmzugkM/tNgbFypNDy5LMVc= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -419,7 +398,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -429,17 +407,15 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= -github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -460,8 +436,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= -github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -473,35 +449,34 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= +github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -529,6 +504,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -537,24 +514,18 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY= -github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -574,22 +545,23 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -601,10 +573,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -615,6 +583,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -641,7 +611,6 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -688,11 +657,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -705,8 +671,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -782,20 +748,14 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -804,10 +764,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -874,15 +832,15 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= -gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -956,7 +914,6 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -977,7 +934,6 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -990,9 +946,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1007,12 +962,9 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1030,6 +982,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1040,25 +993,25 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg= k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY= -k8s.io/api v0.27.7 h1:7yG4D3t/q4utJe2ptlRw9aPuxcSmroTsYxsofkQNl/A= -k8s.io/api v0.27.7/go.mod h1:ZNExI/Lhrs9YrLgVWx6jjHZdoWCTXfBXuFjt1X6olro= +k8s.io/api v0.28.11 h1:2qFr3jSpjy/9QirmlRP0LZeomexuwyRlE8CWUn9hPNY= +k8s.io/api v0.28.11/go.mod h1:nQSGyxQ2sbS73i1zEJyaktFvFfD72z/7nU+LqxzNnXk= k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE= -k8s.io/apiextensions-apiserver v0.27.7 h1:YqIOwZAUokzxJIjunmUd4zS1v3JhK34EPXn+pP0/bsU= -k8s.io/apiextensions-apiserver v0.27.7/go.mod h1:x0p+b5a955lfPz9gaDeBy43obM12s+N9dNHK6+dUL+g= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= -k8s.io/apimachinery v0.27.7 h1:Gxgtb7Y/Rsu8ymgmUEaiErkxa6RY4oTd8kNUI6SUR58= -k8s.io/apimachinery v0.27.7/go.mod h1:jBGQgTjkw99ef6q5hv1YurDd3BqKDk9YRxmX0Ozo0i8= +k8s.io/apimachinery v0.28.11 h1:Ovrx7IOkKSgFJn8+d5BXOC7POzP4i7kOAVlx46iRQ04= +k8s.io/apimachinery v0.28.11/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU= k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk= -k8s.io/client-go v0.27.7 h1:+Xgh9OOKv6A3qdD4Dnl/0VOI5EvAv+0s/OseDxVVTwQ= -k8s.io/client-go v0.27.7/go.mod h1:dZ2kqcalYp5YZ2EV12XIMc77G6PxHWOJp/kclZr4+5Q= +k8s.io/client-go v0.28.11 h1:YHtF6Bg4/DdYHHsx6f5Ti/0giwoo19t3DbBYYmo9xks= +k8s.io/client-go v0.28.11/go.mod h1:yi2BW8PQhFDLGmZ3WbyTJYX5J8YM6n3WUj1fvL7pJ4g= k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= k8s.io/code-generator v0.22.0/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= -k8s.io/component-base v0.27.7 h1:kngM58HR9W9Nqpv7e4rpdRyWnKl/ABpUhLAZ+HoliMs= -k8s.io/component-base v0.27.7/go.mod h1:YGjlCVL1oeKvG3HSciyPHFh+LCjIEqsxz4BDR3cfHRs= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201203183100-97869a43a9d9/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -1068,25 +1021,25 @@ k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.10.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= -k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210820185131-d34e5cb4466e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= -k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/controller-runtime v0.9.6/go.mod h1:q6PpkM5vqQubEKUKOM6qr06oXGzOBcCby1DA9FbyZeA= -sigs.k8s.io/controller-runtime v0.15.3 h1:L+t5heIaI3zeejoIyyvLQs5vTVu/67IU2FfisVzFlBc= -sigs.k8s.io/controller-runtime v0.15.3/go.mod h1:kp4jckA4vTx281S/0Yk2LFEEQe67mjg+ev/yknv47Ds= +sigs.k8s.io/controller-runtime v0.16.5 h1:yr1cEJbX08xsTW6XEIzT13KHHmIyX8Umvme2cULvFZw= +sigs.k8s.io/controller-runtime v0.16.5/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/controller-tools v0.6.2/go.mod h1:oaeGpjXn6+ZSEIQkUe/+3I40PNiDYp9aeawbt3xTgJ8= sigs.k8s.io/gateway-api v0.4.3 h1:9kdHAcfkyP7jVMSFshc8EYEKNLlFM7hbZL8vCKcMwps= sigs.k8s.io/gateway-api v0.4.3/go.mod h1:r3eiNP+0el+NTLwaTfOrCNXy8TukC+dIM3ggc+fbNWk= diff --git a/hack/attach_license.py b/hack/attach_license.py index a2cc19b..09bbef9 100755 --- a/hack/attach_license.py +++ b/hack/attach_license.py @@ -10,7 +10,7 @@ with open('./hack/boilerplate.go.txt') as f: AUTH_COMMENT = f.read() -PATTERN = "/\*.*?Copyright.*?Licensed.*?\*/\s" +PATTERN = r"/\*.*?Licensed under the Apache License.*?\*/\s" EXTENSION = ".go" for root, dirs, files in os.walk('.'): diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index e31736f..75023a9 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/kubernetes/builder/clusterbuilder/acl.go b/internal/builder/clusterbuilder/acl.go similarity index 63% rename from pkg/kubernetes/builder/clusterbuilder/acl.go rename to internal/builder/clusterbuilder/acl.go index b987bda..83ff495 100644 --- a/pkg/kubernetes/builder/clusterbuilder/acl.go +++ b/internal/builder/clusterbuilder/acl.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,12 +18,14 @@ package clusterbuilder import ( "fmt" + "strings" - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/redis/v1" + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/middleware/redis/v1" security "github.com/alauda/redis-operator/pkg/security/password" "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/pkg/types/user" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -35,10 +37,15 @@ func GenerateClusterACLConfigMapName(name string) string { func GenerateClusterACLOperatorSecretName(name string) string { return fmt.Sprintf("drc-acl-%s-operator-secret", name) } + func GenerateClusterOperatorsRedisUserName(name string) string { return fmt.Sprintf("drc-acl-%s-operator", name) } +func GenerateClusterRedisUserName(instName, name string) string { + return fmt.Sprintf("drc-acl-%s-%s", instName, name) +} + func GenerateClusterOperatorsRedisUser(rc types.RedisClusterInstance, passwordsecret string) redismiddlewarealaudaiov1.RedisUser { passwordsecrets := []string{} if passwordsecret != "" { @@ -55,7 +62,7 @@ func GenerateClusterOperatorsRedisUser(rc types.RedisClusterInstance, passwordse }, Spec: redismiddlewarealaudaiov1.RedisUserSpec{ AccountType: redismiddlewarealaudaiov1.System, - Arch: redis.ClusterArch, + Arch: core.RedisCluster, RedisName: rc.GetName(), Username: "operator", PasswordSecrets: passwordsecrets, @@ -68,23 +75,44 @@ func GenerateClusterDefaultRedisUserName(name string) string { return fmt.Sprintf("drc-acl-%s-default", name) } -func GenerateClusterDefaultRedisUser(drc *redisv1alpha1.DistributedRedisCluster, passwordsecret string) redismiddlewarealaudaiov1.RedisUser { - passwordsecrets := []string{} - if passwordsecret != "" { - passwordsecrets = append(passwordsecrets, passwordsecret) +func GenerateClusterRedisUser(obj metav1.Object, u *user.User) *redismiddlewarealaudaiov1.RedisUser { + var ( + name = GenerateClusterRedisUserName(obj.GetName(), u.Name) + accountType redismiddlewarealaudaiov1.AccountType + passwordSecrets []string + ) + switch u.Role { + case user.RoleOperator: + accountType = redismiddlewarealaudaiov1.System + default: + if u.Name == "default" { + accountType = redismiddlewarealaudaiov1.Default + } else { + accountType = redismiddlewarealaudaiov1.Custom + } } - return redismiddlewarealaudaiov1.RedisUser{ + if u.GetPassword().GetSecretName() != "" { + passwordSecrets = append(passwordSecrets, u.GetPassword().GetSecretName()) + } + var rules []string + for _, rule := range u.Rules { + rules = append(rules, rule.Encode()) + } + + return &redismiddlewarealaudaiov1.RedisUser{ ObjectMeta: metav1.ObjectMeta{ - Name: GenerateClusterDefaultRedisUserName(drc.Name), - Namespace: drc.Namespace, + Name: name, + Namespace: obj.GetNamespace(), + Annotations: map[string]string{}, + Labels: map[string]string{}, }, Spec: redismiddlewarealaudaiov1.RedisUserSpec{ - AccountType: redismiddlewarealaudaiov1.Default, - Arch: redis.ClusterArch, - RedisName: drc.Name, - Username: "default", - PasswordSecrets: passwordsecrets, - AclRules: "allkeys +@all -acl -flushall -flushdb -keys", + AccountType: accountType, + Arch: core.RedisCluster, + RedisName: obj.GetName(), + Username: u.Name, + PasswordSecrets: passwordSecrets, + AclRules: strings.Join(rules, " "), }, } } diff --git a/pkg/kubernetes/builder/clusterbuilder/certificate.go b/internal/builder/clusterbuilder/certificate.go similarity index 66% rename from pkg/kubernetes/builder/clusterbuilder/certificate.go rename to internal/builder/clusterbuilder/certificate.go index 04efd67..0cc8d4c 100644 --- a/pkg/kubernetes/builder/clusterbuilder/certificate.go +++ b/internal/builder/clusterbuilder/certificate.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,35 +17,21 @@ limitations under the License. package clusterbuilder import ( - "fmt" "time" - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/util" + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" certv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" v12 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// GenerateCertName -func GenerateCertName(name string) string { - return name + "-cert" -} - -func GetRedisSSLSecretName(name string) string { - return fmt.Sprintf("%s-tls", name) -} - -// GetServiceDNSName -func GetServiceDNSName(serviceName, namespace string) string { - return fmt.Sprintf("%s.%s.svc", serviceName, namespace) -} - // NewCertificate func NewCertificate(drc *redisv1alpha1.DistributedRedisCluster) *certv1.Certificate { return &certv1.Certificate{ ObjectMeta: metav1.ObjectMeta{ - Name: GenerateCertName(drc.Name), + Name: builder.GenerateCertName(drc.Name), Namespace: drc.Namespace, Labels: GetClusterLabels(drc.GetName(), nil), OwnerReferences: util.BuildOwnerReferences(drc), @@ -54,10 +40,11 @@ func NewCertificate(drc *redisv1alpha1.DistributedRedisCluster) *certv1.Certific // 10 year Duration: &metav1.Duration{Duration: 87600 * time.Hour}, DNSNames: []string{ - GetServiceDNSName(drc.Spec.ServiceName, drc.Namespace), + builder.GetServiceDNSName(drc.Spec.ServiceName, drc.Namespace), + builder.GetServiceDNSName(RedisProxySvcName(drc.Name), drc.Namespace), }, IssuerRef: v12.ObjectReference{Kind: certv1.ClusterIssuerKind, Name: "cpaas-ca"}, - SecretName: GetRedisSSLSecretName(drc.Name), + SecretName: builder.GetRedisSSLSecretName(drc.Name), }, } } diff --git a/pkg/kubernetes/builder/clusterbuilder/configmap.go b/internal/builder/clusterbuilder/configmap.go similarity index 84% rename from pkg/kubernetes/builder/clusterbuilder/configmap.go rename to internal/builder/clusterbuilder/configmap.go index afc83d5..698cae1 100644 --- a/pkg/kubernetes/builder/clusterbuilder/configmap.go +++ b/internal/builder/clusterbuilder/configmap.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -21,21 +21,19 @@ import ( "fmt" "reflect" "sort" - "strconv" "strings" - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/util" + "github.com/samber/lo" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( - RestoreSucceeded = "succeeded" - RedisConfKey = "redis.conf" ) @@ -55,9 +53,6 @@ func NewConfigMapForCR(cluster types.RedisClusterInstance) (*corev1.ConfigMap, e }, Data: map[string]string{ RedisConfKey: redisConfContent, - // 跳过pod异常升级重启失败的问题 - "shutdown.sh": "echo skip", - "fix-ip.sh": "echo skip", }, }, nil } @@ -72,16 +67,6 @@ const ( RedisConfig_ReplDisklessSync = "repl-diskless-sync" ) -var MustQuoteRedisConfig = map[string]struct{}{ - "tls-protocols": {}, -} - -var MustUpperRedisConfig = map[string]struct{}{ - "tls-ciphers": {}, - "tls-ciphersuites": {}, - "tls-protocols": {}, -} - var ForbidToRenameCommands = map[string]struct{}{ "config": {}, "cluster": {}, @@ -96,8 +81,8 @@ func buildRedisConfigs(cluster types.RedisClusterInstance) (string, error) { var ( keys = make([]string, 0, len(cr.Spec.Config)) - innerRedisConfig = cluster.Version().CustomConfigs(redis.ClusterArch) - configMap = util.MergeMap(cr.Spec.Config, innerRedisConfig) + innerRedisConfig = cluster.Version().CustomConfigs(core.RedisCluster) + configMap = lo.Assign(cr.Spec.Config, innerRedisConfig) ) // check memory-policy @@ -177,7 +162,16 @@ func buildRedisConfigs(cluster types.RedisClusterInstance) (string, error) { for i := 0; i < len(fields); i += 4 { buffer.WriteString(fmt.Sprintf("%s %s %s %s %s\n", k, fields[i], fields[i+1], fields[i+2], fields[i+3])) } - case RedisConfig_Save, RedisConfig_RenameCommand: + case RedisConfig_Save: + fields := strings.Fields(v) + if len(fields)%2 != 0 { + return "", fmt.Errorf(`value "%s" for config %s is invalid`, v, k) + } + for i := 0; i < len(fields); i += 2 { + buffer.WriteString(fmt.Sprintf("%s %s %s\n", k, fields[i], fields[i+1])) + } + case RedisConfig_RenameCommand: + // DEPRECATED: for consistence of the config fields := strings.Fields(v) if len(fields)%2 != 0 { return "", fmt.Errorf(`value "%s" for config %s is invalid`, v, k) @@ -186,10 +180,10 @@ func buildRedisConfigs(cluster types.RedisClusterInstance) (string, error) { buffer.WriteString(fmt.Sprintf("%s %s %s\n", k, fields[i], fields[i+1])) } default: - if _, ok := MustQuoteRedisConfig[k]; ok && !strings.HasPrefix(v, `"`) { + if _, ok := builder.MustQuoteRedisConfig[k]; ok && !strings.HasPrefix(v, `"`) { v = fmt.Sprintf(`"%s"`, v) } - if _, ok := MustUpperRedisConfig[k]; ok { + if _, ok := builder.MustUpperRedisConfig[k]; ok { v = strings.ToUpper(v) } buffer.WriteString(fmt.Sprintf("%s %s\n", k, v)) @@ -202,24 +196,6 @@ func RedisConfigMapName(clusterName string) string { return fmt.Sprintf("%s-%s", "redis-cluster", clusterName) } -func NewConfigMapForRestore(cluster *redisv1alpha1.DistributedRedisCluster, labels map[string]string) *corev1.ConfigMap { - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: RestoreConfigMapName(cluster.Name), - Namespace: cluster.Namespace, - Labels: labels, - OwnerReferences: util.BuildOwnerReferences(cluster), - }, - Data: map[string]string{ - RestoreSucceeded: strconv.Itoa(0), - }, - } -} - -func RestoreConfigMapName(clusterName string) string { - return fmt.Sprintf("%s-%s", "rediscluster-restore", clusterName) -} - type RedisConfigSettingRule string const ( @@ -347,22 +323,37 @@ func (o RedisConfig) Diff(n RedisConfig) (added, changed, deleted map[string]Red } func ParseRenameConfigs(val string) (ret map[string]string, err error) { + ret = map[string]string{} if val == "" { return } val = strings.ToLower(strings.TrimSpace(val)) - ret = map[string]string{} fields := strings.Fields(val) if len(fields)%2 == 0 { for i := 0; i < len(fields); i += 2 { - if fields[i+1] == "" { - fields[i+1] = `""` + k, v := fields[i], fields[i+1] + for _, c := range []string{"\"", "'"} { + if strings.HasPrefix(k, c) && strings.HasSuffix(k, c) { + k = strings.TrimPrefix(strings.TrimSuffix(k, c), c) + } + if strings.HasPrefix(v, c) && strings.HasSuffix(v, c) { + v = strings.TrimPrefix(strings.TrimSuffix(v, c), c) + } + } + if v == "" { + v = `""` } - ret[fields[i]] = fields[i+1] + ret[k] = v } } else { err = fmt.Errorf("invalid rename value %s", val) } + + for k, v := range ret { + if k == v { + delete(ret, k) + } + } return } diff --git a/internal/builder/clusterbuilder/configmap_test.go b/internal/builder/clusterbuilder/configmap_test.go new file mode 100644 index 0000000..4e140f3 --- /dev/null +++ b/internal/builder/clusterbuilder/configmap_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clusterbuilder + +import ( + "reflect" + "testing" +) + +func TestParseRenameConfigs(t *testing.T) { + type args struct { + val string + } + tests := []struct { + name string + args args + wantRet map[string]string + wantErr bool + }{ + { + name: "forbid", + args: args{`flushall "" flushdb ""`}, + wantRet: map[string]string{"flushall": `""`, "flushdb": `""`}, + wantErr: false, + }, + { + name: "disable forbid", + args: args{``}, + wantRet: map[string]string{}, + wantErr: false, + }, + { + name: "disable forbid 2", + args: args{`flushall "" flushdb "" flushall "flushall" flushdb "flushdb"`}, + wantRet: map[string]string{}, + wantErr: false, + }, + { + name: "rules with qoutes", + args: args{`set "abc"`}, + wantRet: map[string]string{"set": "abc"}, + wantErr: false, + }, + { + name: "rules with qoutes", + args: args{`set 'abc'`}, + wantRet: map[string]string{"set": "abc"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRet, err := ParseRenameConfigs(tt.args.val) + if (err != nil) != tt.wantErr { + t.Errorf("ParseRenameConfigs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotRet, tt.wantRet) { + t.Errorf("ParseRenameConfigs() = %v, want %v", gotRet, tt.wantRet) + } + }) + } +} diff --git a/pkg/kubernetes/builder/clusterbuilder/helper.go b/internal/builder/clusterbuilder/helper.go similarity index 93% rename from pkg/kubernetes/builder/clusterbuilder/helper.go rename to internal/builder/clusterbuilder/helper.go index aa10891..8a7e35f 100644 --- a/pkg/kubernetes/builder/clusterbuilder/helper.go +++ b/internal/builder/clusterbuilder/helper.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,7 +20,8 @@ import ( "context" "fmt" - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/internal/builder" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" @@ -36,10 +37,10 @@ const ( // NOTE: this labels are const, take care of edit this func getPublicLabels(name string) map[string]string { return map[string]string{ - "managed-by": "redis-cluster-operator", "redis.kun/name": name, "middleware.instance/type": "distributed-redis-cluster", - "middleware.instance/name": name, + builder.InstanceNameLabel: name, + builder.ManagedByLabel: "redis-cluster-operator", } } diff --git a/pkg/kubernetes/builder/clusterbuilder/pod.go b/internal/builder/clusterbuilder/pod.go similarity index 97% rename from pkg/kubernetes/builder/clusterbuilder/pod.go rename to internal/builder/clusterbuilder/pod.go index 0edd9bf..f741d8a 100644 --- a/pkg/kubernetes/builder/clusterbuilder/pod.go +++ b/internal/builder/clusterbuilder/pod.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,7 +19,7 @@ package clusterbuilder import ( "reflect" - "github.com/alauda/redis-operator/pkg/util" + "github.com/alauda/redis-operator/internal/util" "github.com/go-logr/logr" v1 "k8s.io/api/core/v1" ) @@ -140,7 +140,7 @@ func loadEnvs(envs []v1.EnvVar) map[string]string { case item.ValueFrom.ResourceFieldRef != nil: kvs[item.Name] = item.ValueFrom.ResourceFieldRef.Resource } - } else if item.Value != "" { + } else { kvs[item.Name] = item.Value } } diff --git a/pkg/kubernetes/builder/clusterbuilder/poddisruptionbudget.go b/internal/builder/clusterbuilder/poddisruptionbudget.go similarity index 76% rename from pkg/kubernetes/builder/clusterbuilder/poddisruptionbudget.go rename to internal/builder/clusterbuilder/poddisruptionbudget.go index 6a2b532..8da5a86 100644 --- a/pkg/kubernetes/builder/clusterbuilder/poddisruptionbudget.go +++ b/internal/builder/clusterbuilder/poddisruptionbudget.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -21,13 +21,18 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/util" + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/internal/util" ) -func NewPodDisruptionBudgetForCR(cluster *redisv1alpha1.DistributedRedisCluster, name string, labels map[string]string) *policyv1.PodDisruptionBudget { +func NewPodDisruptionBudgetForCR(cluster *redisv1alpha1.DistributedRedisCluster, index int) *policyv1.PodDisruptionBudget { maxUnavailable := intstr.FromInt(1) + var ( + name = ClusterStatefulSetName(cluster.Name, index) + labels = GetClusterStatefulsetSelectorLabels(cluster.Name, index) + ) + return &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, diff --git a/internal/builder/clusterbuilder/redisproxy.go b/internal/builder/clusterbuilder/redisproxy.go new file mode 100644 index 0000000..46c77b4 --- /dev/null +++ b/internal/builder/clusterbuilder/redisproxy.go @@ -0,0 +1,53 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clusterbuilder + +import ( + "fmt" + + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/internal/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + DefaultRedisProxyPort = 7777 +) + +func RedisProxySvcName(clusterName string) string { + return fmt.Sprintf("%s-proxy", clusterName) +} + +func NewProxySvcForCR(cluster *redisv1alpha1.DistributedRedisCluster, labels map[string]string) *corev1.Service { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Name: RedisProxySvcName(cluster.Name), + Namespace: cluster.Namespace, + OwnerReferences: util.BuildOwnerReferences(cluster), + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Name: "redis", Port: 6379, TargetPort: intstr.FromInt(DefaultRedisProxyPort)}, + }, + Selector: labels, + }, + } + return svc +} diff --git a/pkg/kubernetes/builder/clusterbuilder/service.go b/internal/builder/clusterbuilder/service.go similarity index 75% rename from pkg/kubernetes/builder/clusterbuilder/service.go rename to internal/builder/clusterbuilder/service.go index 1910618..0712260 100644 --- a/pkg/kubernetes/builder/clusterbuilder/service.go +++ b/internal/builder/clusterbuilder/service.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,18 +19,15 @@ package clusterbuilder import ( "fmt" - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/util" + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const ( - LabelRedisArch = "redisarch" -) - // NewHeadlessSvcForCR creates a new headless service for the given Cluster. func NewHeadlessSvcForCR(cluster *redisv1alpha1.DistributedRedisCluster, index int) *corev1.Service { name := ClusterHeadlessSvcName(cluster.Spec.ServiceName, index) @@ -63,11 +60,10 @@ func NewHeadlessSvcForCR(cluster *redisv1alpha1.DistributedRedisCluster, index i } func NewServiceForCR(cluster *redisv1alpha1.DistributedRedisCluster) *corev1.Service { - name := cluster.Name selectors := GetClusterStatefulsetSelectorLabels(cluster.Name, -1) labels := GetClusterStatefulsetSelectorLabels(cluster.Name, -1) // Set redis arch label, for identifying redis arch in prometheus, so wo can find redis metrics data for redis cluster only. - labels[LabelRedisArch] = string(redis.ClusterArch) + labels[builder.LabelRedisArch] = string(core.RedisCluster) ptype := corev1.IPFamilyPolicySingleStack protocol := []corev1.IPFamily{} if cluster.Spec.IPFamilyPrefer == corev1.IPv6Protocol { @@ -81,13 +77,13 @@ func NewServiceForCR(cluster *redisv1alpha1.DistributedRedisCluster) *corev1.Ser ports = append(ports, clientPort, gossipPort) if cluster.Spec.Monitor != nil { - ports = append(ports, corev1.ServicePort{Name: "prom-http", Port: PrometheusExporterPort}) + ports = append(ports, corev1.ServicePort{Name: "prom-http", Port: PrometheusExporterPortNumber}) } svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, - Name: name, + Name: cluster.Name, Namespace: cluster.Namespace, OwnerReferences: util.BuildOwnerReferences(cluster), }, @@ -104,7 +100,7 @@ func NewServiceForCR(cluster *redisv1alpha1.DistributedRedisCluster) *corev1.Ser func NewNodeportSvc(cluster *redisv1alpha1.DistributedRedisCluster, name string, labels map[string]string, port int32) *corev1.Service { clientPort := corev1.ServicePort{Name: "client", Port: 6379, NodePort: port} selectorLabels := map[string]string{ - "statefulset.kubernetes.io/pod-name": name, + builder.PodNameLabelKey: name, } ptype := corev1.IPFamilyPolicySingleStack protocol := []corev1.IPFamily{} @@ -131,13 +127,47 @@ func NewNodeportSvc(cluster *redisv1alpha1.DistributedRedisCluster, name string, return svc } -func NewNodePortServiceForCR(cluster *redisv1alpha1.DistributedRedisCluster) *corev1.Service { +func NewPodService(cluster *redisv1alpha1.DistributedRedisCluster, name string, typ corev1.ServiceType, + labels map[string]string, annotations map[string]string) *corev1.Service { + + clientPort := corev1.ServicePort{Name: "client", Port: 6379} + gossipPort := corev1.ServicePort{Name: "gossip", Port: 16379} + selectorLabels := map[string]string{ + builder.PodNameLabelKey: name, + } + ptype := corev1.IPFamilyPolicySingleStack + protocol := []corev1.IPFamily{} + if cluster.Spec.IPFamilyPrefer == corev1.IPv6Protocol { + protocol = append(protocol, corev1.IPv6Protocol) + } else { + protocol = append(protocol, corev1.IPv4Protocol) + } + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + Name: name, + Namespace: cluster.Namespace, + OwnerReferences: util.BuildOwnerReferences(cluster), + }, + Spec: corev1.ServiceSpec{ + IPFamilies: protocol, + IPFamilyPolicy: &ptype, + Ports: []corev1.ServicePort{clientPort, gossipPort}, + Selector: selectorLabels, + Type: typ, + }, + } + return svc +} + +func NewServiceWithType(cluster *redisv1alpha1.DistributedRedisCluster, typ corev1.ServiceType, port int32) *corev1.Service { name := RedisNodePortSvcName(cluster.Name) selectors := GetClusterStatefulsetSelectorLabels(cluster.Name, -1) labels := GetClusterStatefulsetSelectorLabels(cluster.Name, -1) // TODO: remove this // Set redis arch label, for identifying redis arch in prometheus, so wo can find redis metrics data for redis cluster only. - labels[LabelRedisArch] = string(redis.ClusterArch) + labels[builder.LabelRedisArch] = string(core.RedisCluster) ptype := corev1.IPFamilyPolicySingleStack protocol := []corev1.IPFamily{} if cluster.Spec.IPFamilyPrefer == corev1.IPv6Protocol { @@ -146,7 +176,7 @@ func NewNodePortServiceForCR(cluster *redisv1alpha1.DistributedRedisCluster) *co protocol = append(protocol, corev1.IPv4Protocol) } var ports []corev1.ServicePort - clientPort := corev1.ServicePort{Name: "client", Port: 6379, Protocol: "TCP", NodePort: cluster.Spec.Expose.AccessPort} + clientPort := corev1.ServicePort{Name: "client", Port: 6379, Protocol: "TCP", NodePort: port} ports = append(ports, clientPort) svc := &corev1.Service{ @@ -154,12 +184,13 @@ func NewNodePortServiceForCR(cluster *redisv1alpha1.DistributedRedisCluster) *co Labels: labels, Name: name, Namespace: cluster.Namespace, + Annotations: cluster.Spec.Expose.Annotations, OwnerReferences: util.BuildOwnerReferences(cluster), }, Spec: corev1.ServiceSpec{ + Type: typ, IPFamilies: protocol, IPFamilyPolicy: &ptype, - Type: corev1.ServiceTypeNodePort, Ports: ports, Selector: selectors, }, diff --git a/pkg/kubernetes/builder/clusterbuilder/serviceaccount.go b/internal/builder/clusterbuilder/serviceaccount.go similarity index 90% rename from pkg/kubernetes/builder/clusterbuilder/serviceaccount.go rename to internal/builder/clusterbuilder/serviceaccount.go index 4c0e673..66cc355 100644 --- a/pkg/kubernetes/builder/clusterbuilder/serviceaccount.go +++ b/internal/builder/clusterbuilder/serviceaccount.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,8 +19,6 @@ package clusterbuilder import ( "fmt" - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - appv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -60,18 +58,13 @@ func NewRole(obj client.Object) *rbacv1.Role { { APIGroups: []string{corev1.GroupName}, Resources: []string{"pods", "pods/exec"}, - Verbs: []string{"get", "list", "watch", "patch"}, + Verbs: []string{"create", "get", "list", "watch", "patch"}, }, { APIGroups: []string{appv1.GroupName}, Resources: []string{"statefulsets"}, Verbs: []string{"get"}, }, - { - APIGroups: []string{redisbackupv1.GroupVersion.Group}, - Resources: []string{"*"}, - Verbs: []string{"get", "create", "list", "patch", "update"}, - }, }, } diff --git a/pkg/kubernetes/builder/clusterbuilder/servicemonitor.go b/internal/builder/clusterbuilder/servicemonitor.go similarity index 84% rename from pkg/kubernetes/builder/clusterbuilder/servicemonitor.go rename to internal/builder/clusterbuilder/servicemonitor.go index 5201d87..e988f29 100644 --- a/pkg/kubernetes/builder/clusterbuilder/servicemonitor.go +++ b/internal/builder/clusterbuilder/servicemonitor.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,39 +17,15 @@ limitations under the License. package clusterbuilder import ( - "fmt" - "os" - "strings" - - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/internal/builder" smv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" ) -const ( - DefaultScrapInterval = "60s" - DefaultScrapeTimeout = "10s" -) - -func getDefaultRegex(regex []string) string { - return fmt.Sprintf("(%s)", strings.Join(uniqueArr(regex), "|")) -} - -func uniqueArr(m []string) []string { - d := make([]string, 0) - result := make(map[string]bool, len(m)) - for _, v := range m { - if !result[v] { - result[v] = true - d = append(d, v) - } - } - return d -} - var regexArr = []string{ "redis_instance_info", + "redis_master_link_up", "redis_slave_info", "redis_connected_clients", "redis_uptime_in_seconds", @@ -101,14 +77,10 @@ var regexArr = []string{ "redis_cluster_.*", } -func GetPodOwnerReferences() metav1.OwnerReference { - return metav1.OwnerReference{ - APIVersion: "v1", - Kind: "Pod", - Name: os.Getenv("POD_NAME"), - UID: types.UID(os.Getenv("POD_UID")), - } -} +const ( + DefaultScrapInterval = "60s" + DefaultScrapeTimeout = "10s" +) const ( RedisClusterServiceMonitorName = "redis-cluster" @@ -116,7 +88,7 @@ const ( func NewServiceMonitorForCR(cluster *redisv1alpha1.DistributedRedisCluster) *smv1.ServiceMonitor { labels := map[string]string{ - "managed-by": "redis-cluster-operator", + builder.ManagedByLabel: "redis-cluster-operator", } interval := DefaultScrapInterval @@ -124,11 +96,11 @@ func NewServiceMonitorForCR(cluster *redisv1alpha1.DistributedRedisCluster) *smv configs := []*smv1.RelabelConfig{{ Action: "keep", - Regex: getDefaultRegex(regexArr), + Regex: builder.BuildMetricsRegex(regexArr), SourceLabels: []smv1.LabelName{"__name__"}, }} - if cluster != nil { + if cluster != nil && cluster.Spec.ServiceMonitor != nil { if cluster.Spec.ServiceMonitor.Interval != "" { interval = cluster.Spec.ServiceMonitor.Interval } diff --git a/pkg/kubernetes/builder/clusterbuilder/statefulset.go b/internal/builder/clusterbuilder/statefulset.go similarity index 69% rename from pkg/kubernetes/builder/clusterbuilder/statefulset.go rename to internal/builder/clusterbuilder/statefulset.go index 668ac1b..6b41241 100644 --- a/pkg/kubernetes/builder/clusterbuilder/statefulset.go +++ b/internal/builder/clusterbuilder/statefulset.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,10 +17,9 @@ limitations under the License. package clusterbuilder import ( + "crypto/sha1" // #nosec "fmt" "net" - "os" - "path" "reflect" "strconv" "strings" @@ -30,14 +29,16 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/pointer" - redisv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/config" + redisv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" ) @@ -51,12 +52,8 @@ const ( hostnameTopologyKey = "kubernetes.io/hostname" - // DNS - LocalInjectName = "local.inject" - // Container CheckContainerName = "init" - RestoreContainerName = "restore" ServerContainerName = "redis" ExporterContainerName = "exporter" ConfigSyncContainerName = "sidecar" @@ -68,17 +65,12 @@ const ( RedisOperatorPasswordVolumeName = "operator-password" ConfigVolumeName = "conf" RedisTLSVolumeName = "redis-tls" - RedisBackupVolumeName = "backup-data" - RedisRestoreLocalVolumeName = "redis-local" RedisOptVolumeName = "redis-opt" - // Mount path StorageVolumeMountPath = "/data" OperatorPasswordVolumeMountPath = "/account" ConfigVolumeMountPath = "/conf" TLSVolumeMountPath = "/tls" - BackupVolumeMountPath = "/backup" - RestoreLocalVolumeMountPath = "/restore" RedisOptVolumeMountPath = "/opt" RedisTmpVolumeMountPath = "/tmp" @@ -86,7 +78,7 @@ const ( OperatorUsername = "OPERATOR_USERNAME" OperatorSecretName = "OPERATOR_SECRET_NAME" - PrometheusExporterPort = 9100 + PrometheusExporterPortNumber = 9100 PrometheusExporterTelemetryPath = "/metrics" ) @@ -96,7 +88,7 @@ const ( ) // NewStatefulSetForCR creates a new StatefulSet for the given Cluster. -func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLSupported bool, backup *redisbackupv1.RedisClusterBackup, index int) (*appsv1.StatefulSet, error) { +func NewStatefulSetForCR(c types.RedisClusterInstance, isAllACLSupported bool, index int) (*appsv1.StatefulSet, error) { cluster := c.Definition() var ( @@ -120,11 +112,6 @@ func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLS size = spec.ClusterReplicas + 1 ) - // NOTE: old cluster use this logic, as if it's useless - // if restoring { - // size = 0 - // } - envs := []corev1.EnvVar{ { Name: "NAMESPACE", @@ -202,7 +189,7 @@ func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLS }, { Name: "NODEPORT_ENABLED", - Value: fmt.Sprintf("%t", cluster.Spec.Expose.EnableNodePort), + Value: fmt.Sprintf("%t", cluster.Spec.Expose.ServiceType == corev1.ServiceTypeNodePort), }, { Name: "IP_FAMILY_PREFER", @@ -210,15 +197,28 @@ func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLS }, { Name: "REDIS_ADDRESS", - Value: net.JoinHostPort(LocalInjectName, strconv.FormatInt(DefaultRedisServerPort, 10)), + Value: net.JoinHostPort(config.LocalInjectName, strconv.FormatInt(DefaultRedisServerPort, 10)), + }, + { + Name: "SERVICE_TYPE", + Value: string(cluster.Spec.Expose.ServiceType), }, } - if cluster.Spec.Expose.EnableNodePort && len(cluster.Spec.Expose.DataStorageNodePortSequence) > 0 { + if cluster.Spec.Expose.ServiceType == corev1.ServiceTypeNodePort && len(cluster.Spec.Expose.NodePortSequence) > 0 { envs = append(envs, corev1.EnvVar{ Name: "CUSTOM_PORT_ENABLED", Value: "true", }) } + if c.Version().IsClusterShardSupported() { + // TODO: use real shard id in redis7 + data := sha1.Sum([]byte(fmt.Sprintf("%s/%s", cluster.Namespace, stsName))) // #nosec + shardId := fmt.Sprintf("%x", data) + envs = append(envs, corev1.EnvVar{ + Name: "SHARD_ID", + Value: shardId, + }) + } if cluster.Spec.EnableTLS { envs = append(envs, @@ -258,13 +258,13 @@ func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLS Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, - Annotations: cluster.Spec.Annotations, + Annotations: cluster.Spec.PodAnnotations, }, Spec: corev1.PodSpec{ HostAliases: []corev1.HostAlias{ { IP: localhost, - Hostnames: []string{LocalInjectName}, + Hostnames: []string{config.LocalInjectName}, }, }, TerminationGracePeriodSeconds: pointer.Int64(DefaultTerminationGracePeriodSeconds), @@ -283,36 +283,6 @@ func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLS }, } - // restore container - // Keep this the first init container - if cluster.Spec.Restore.BackupName != "" { - if backup.Spec.Target.S3Option.S3Secret != "" { - s3secretVolumes := corev1.Volume{ - Name: util.S3SecretVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{SecretName: backup.Spec.Target.S3Option.S3Secret}, - }, - } - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, s3secretVolumes) - restore := createRestoreContainerV2(cluster, backup, index) - restorev2 := createRestoreContainerV2NodeConfig(cluster, backup, index) - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, restore, restorev2) - } else { - restore := createRestoreContainer(cluster, index) - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, restore) - - backupVolumes := corev1.Volume{ - Name: RedisBackupVolumeName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: GetClaimName(backup.Status.Destination), - }, - }, - } - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, backupVolumes) - } - } - if spec.Storage != nil && spec.Storage.Type == redisv1alpha1.PersistentClaim { ss.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{ persistentClaim(cluster, labels), @@ -323,7 +293,7 @@ func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLS } } - initContainer, container := initContainer(cluster, opUser, envs) + initContainer, container := buildContainers(cluster, opUser, envs) if initContainer != nil && container != nil { ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, *initContainer) ss.Spec.Template.Spec.Containers = append(ss.Spec.Template.Spec.Containers, *container) @@ -336,6 +306,10 @@ func NewStatefulSetForCR(c types.RedisClusterInstance, restoring bool, isAllACLS } func persistentClaim(cluster *redisv1alpha1.DistributedRedisCluster, labels map[string]string) corev1.PersistentVolumeClaim { + var sc *string + if cluster.Spec.Storage.Class != "" { + sc = &cluster.Spec.Storage.Class + } mode := corev1.PersistentVolumeFilesystem return corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -349,31 +323,29 @@ func persistentClaim(cluster *redisv1alpha1.DistributedRedisCluster, labels map[ corev1.ResourceStorage: cluster.Spec.Storage.Size, }, }, - StorageClassName: &cluster.Spec.Storage.Class, + StorageClassName: sc, VolumeMode: &mode, }, } } func redisServerContainer(cluster *redisv1alpha1.DistributedRedisCluster, u *user.User, envs []corev1.EnvVar, index int) corev1.Container { - // NOTE: use ping to escape password hotconfig error for old versions - probeArgs := []string{"redis-cli", "-h", LocalInjectName, "ping"} - livenessProbe := []string{"/opt/redis-tools", "cluster", "healthcheck", "-t", "4", "liveness"} - readinessProbe := []string{"/opt/redis-tools", "cluster", "healthcheck", "-t", "4", "liveness"} - // use new probe commands for operator user - if u == nil || u.Role != user.RoleOperator { - livenessProbe = probeArgs - readinessProbe = probeArgs - } - shutdownArgs := []string{"sh", "-c", "/opt/redis-tools cluster shutdown &> /proc/1/fd/1"} startArgs := []string{"sh", "/opt/run.sh"} + if cluster.Spec.EnableActiveRedis { + startArgs = append(startArgs, + "--loadmodule", "/modules/activeredis.so", + "service_id", fmt.Sprintf("%d", *cluster.Spec.ServiceID), + "service_uid", string(cluster.UID), + "shard_id", fmt.Sprintf("%d", index), + ) + } container := corev1.Container{ Env: envs, Name: ServerContainerName, Image: cluster.Spec.Image, - ImagePullPolicy: pullPolicy(cluster.Spec.ImagePullPolicy), + ImagePullPolicy: builder.GetPullPolicy(cluster.Spec.ImagePullPolicy), Command: startArgs, SecurityContext: getContainerSecurityContext(cluster.Spec.ContainerSecurityContext), Ports: []corev1.ContainerPort{ @@ -392,30 +364,33 @@ func redisServerContainer(cluster *redisv1alpha1.DistributedRedisCluster, u *use StartupProbe: &corev1.Probe{ InitialDelaySeconds: ProbeDelaySeconds, TimeoutSeconds: 5, - FailureThreshold: 30, + FailureThreshold: 5, ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: livenessProbe, + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(DefaultRedisServerPort), }, }, }, LivenessProbe: &corev1.Probe{ - InitialDelaySeconds: ProbeDelaySeconds, - TimeoutSeconds: 5, - FailureThreshold: 5, + InitialDelaySeconds: 10, + PeriodSeconds: 10, + TimeoutSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, ProbeHandler: corev1.ProbeHandler{ Exec: &corev1.ExecAction{ - Command: livenessProbe, + Command: []string{"/opt/redis-tools", "helper", "healthcheck", "ping"}, }, }, }, ReadinessProbe: &corev1.Probe{ - InitialDelaySeconds: ProbeDelaySeconds, + InitialDelaySeconds: 10, + PeriodSeconds: 10, TimeoutSeconds: 5, FailureThreshold: 3, ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: readinessProbe, + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(DefaultRedisServerPort), }, }, }, @@ -428,13 +403,13 @@ func redisServerContainer(cluster *redisv1alpha1.DistributedRedisCluster, u *use }, }, } - container.Env = customContainerEnv(container.Env, cluster.Spec.Env) + container.Env = append(container.Env, cluster.Spec.Env...) return container } -func initContainer(cluster *redisv1alpha1.DistributedRedisCluster, user *user.User, envs []corev1.EnvVar) (*corev1.Container, *corev1.Container) { - image := config.GetRedisToolsImage() +func buildContainers(cluster *redisv1alpha1.DistributedRedisCluster, user *user.User, envs []corev1.EnvVar) (*corev1.Container, *corev1.Container) { + image := config.GetRedisToolsImage(cluster) if image == "" { return nil, nil } @@ -442,7 +417,7 @@ func initContainer(cluster *redisv1alpha1.DistributedRedisCluster, user *user.Us initContainer := corev1.Container{ Name: CheckContainerName, Image: image, - ImagePullPolicy: corev1.PullAlways, + ImagePullPolicy: builder.GetPullPolicy(cluster.Spec.ImagePullPolicy), Env: envs, Command: []string{"sh", "-c", "/opt/init.sh"}, Resources: corev1.ResourceRequirements{ @@ -508,16 +483,16 @@ func redisExporterContainer(cluster *redisv1alpha1.DistributedRedisCluster, user container := corev1.Container{ Name: ExporterContainerName, Args: append([]string{ - fmt.Sprintf("--web.listen-address=:%v", PrometheusExporterPort), + fmt.Sprintf("--web.listen-address=:%v", PrometheusExporterPortNumber), fmt.Sprintf("--web.telemetry-path=%v", PrometheusExporterTelemetryPath), }, cluster.Spec.Monitor.Args...), Image: cluster.Spec.Monitor.Image, - ImagePullPolicy: corev1.PullAlways, + ImagePullPolicy: builder.GetPullPolicy(cluster.Spec.Monitor.ImagePullPolicy, cluster.Spec.ImagePullPolicy), Ports: []corev1.ContainerPort{ { Name: "prom-http", Protocol: corev1.ProtocolTCP, - ContainerPort: PrometheusExporterPort, + ContainerPort: PrometheusExporterPortNumber, }, }, Env: cluster.Spec.Monitor.Env, @@ -568,13 +543,13 @@ func redisExporterContainer(cluster *redisv1alpha1.DistributedRedisCluster, user { Name: "REDIS_ADDR", // NOTE: use dns to escape ipv4/ipv6 check - Value: fmt.Sprintf("rediss://%s:%d", LocalInjectName, DefaultRedisServerPort), + Value: fmt.Sprintf("rediss://%s:%d", config.LocalInjectName, DefaultRedisServerPort), }, }...) } else if cluster.Spec.IPFamilyPrefer == corev1.IPv6Protocol { container.Env = append(container.Env, []corev1.EnvVar{ {Name: "REDIS_ADDR", - Value: fmt.Sprintf("redis://%s:%d", LocalInjectName, DefaultRedisServerPort)}, + Value: fmt.Sprintf("redis://%s:%d", config.LocalInjectName, DefaultRedisServerPort)}, }...) } return container @@ -701,7 +676,7 @@ func redisVolumes(cluster *redisv1alpha1.DistributedRedisCluster, user *user.Use Name: RedisTLSVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: GetRedisSSLSecretName(cluster.Name), + SecretName: builder.GetRedisSSLSecretName(cluster.Name), }, }, }) @@ -721,170 +696,15 @@ func redisDataVolume(cluster *redisv1alpha1.DistributedRedisCluster) *corev1.Vol } switch cluster.Spec.Storage.Type { - case redisv1alpha1.Ephemeral: - return emptyVolume() case redisv1alpha1.PersistentClaim: return nil + case redisv1alpha1.Ephemeral: + return emptyVolume() default: return emptyVolume() } } -func createRestoreContainerV2(cluster *redisv1alpha1.DistributedRedisCluster, rb *redisbackupv1.RedisClusterBackup, index int) corev1.Container { - - image := cluster.Spec.Restore.Image - if image == "" { - image = rb.Spec.Image - } - // 不使用redis-backup 恢复镜像 - if strings.Contains(image, "redis-backup:") { - image = os.Getenv(config.GetDefaultBackupImage()) - } - container := corev1.Container{ - Name: RestoreContainerName, - Image: image, - ImagePullPolicy: pullPolicy(cluster.Spec.Restore.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: RedisStorageVolumeName, - MountPath: "/data", - }, - {Name: util.S3SecretVolumeName, - MountPath: "/s3_secret", - ReadOnly: true, - }, - }, - Env: []corev1.EnvVar{ - {Name: "RDB_CHECK", Value: "true"}, - {Name: "TARGET_FILE", Value: "/data/dump.rdb"}, - {Name: "S3_ENDPOINT", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: rb.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_ENDPOINTURL, - }, - }, - }, - {Name: "S3_REGION", ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: rb.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_REGION}}, - }, - {Name: "S3_BUCKET_NAME", Value: rb.Spec.Target.S3Option.Bucket}, - {Name: "S3_OBJECT_NAME", Value: path.Join(rb.Spec.Target.S3Option.Dir, fmt.Sprintf("%d.rdb", index))}, - }, - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup pull"}, - } - - return container -} - -func createRestoreContainerV2NodeConfig(cluster *redisv1alpha1.DistributedRedisCluster, rb *redisbackupv1.RedisClusterBackup, index int) corev1.Container { - - image := cluster.Spec.Restore.Image - if image == "" { - image = rb.Spec.Image - } - // 不使用redis-backup 恢复镜像 - if strings.Contains(image, "redis-backup:") { - image = os.Getenv(config.GetDefaultBackupImage()) - } - container := corev1.Container{ - Name: RestoreContainerName + "-node-conf", - Image: image, - ImagePullPolicy: pullPolicy(cluster.Spec.Restore.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: RedisStorageVolumeName, - MountPath: "/data", - }, - {Name: util.S3SecretVolumeName, - MountPath: "/s3_secret", - ReadOnly: true, - }, - }, - Env: []corev1.EnvVar{ - {Name: "DOWNLOAD_FILE", Value: fmt.Sprintf("/backup/%d.node.conf", index)}, - {Name: "RDB_CHECK", Value: "false"}, - {Name: "TARGET_FILE", Value: "/data/nodes.conf"}, - {Name: "S3_ENDPOINT", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: rb.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_ENDPOINTURL, - }, - }, - }, - {Name: "S3_REGION", ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: rb.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_REGION}}, - }, - {Name: "S3_BUCKET_NAME", Value: rb.Spec.Target.S3Option.Bucket}, - {Name: "S3_OBJECT_NAME", Value: path.Join(rb.Spec.Target.S3Option.Dir, fmt.Sprintf("%d.node.conf", index))}, - }, - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup pull"}, - } - return container -} - -func createRestoreContainer(cluster *redisv1alpha1.DistributedRedisCluster, index int) corev1.Container { - image := config.GetDefaultBackupImage() - if cluster.Spec.Restore.Image != "" { - image = cluster.Spec.Restore.Image - } - container := corev1.Container{ - Name: RestoreContainerName, - Image: image, - ImagePullPolicy: pullPolicy(cluster.Spec.Restore.ImagePullPolicy), - Env: []corev1.EnvVar{ - {Name: "REDIS_ClUSTER_INDEX", Value: strconv.Itoa(index)}, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: RedisBackupVolumeName, - MountPath: BackupVolumeMountPath, - }, - { - Name: RedisStorageVolumeName, - MountPath: StorageVolumeMountPath, - }, - }, - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup restore"}, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, - }, - } - return container -} - -// split storage name, example: pvc/redisfailover-persistent-keep-data-rfr-redis-sentinel-demo-0 -func GetClaimName(backupDestination string) string { - names := strings.Split(backupDestination, "/") - if len(names) != 2 { - return "" - } - return names[1] -} - func getAffinity(cluster *redisv1alpha1.DistributedRedisCluster, labels map[string]string, ssName string) *corev1.Affinity { affinity := cluster.Spec.Affinity if affinity != nil { @@ -892,8 +712,14 @@ func getAffinity(cluster *redisv1alpha1.DistributedRedisCluster, labels map[stri } policy := cluster.Spec.AffinityPolicy + if policy == "" { + if cluster.Spec.RequiredAntiAffinity { + policy = core.AntiAffinity + } + } + switch policy { - case redisv1alpha1.AntiAffinityInSharding: + case core.AntiAffinityInSharding: return &corev1.Affinity{ PodAntiAffinity: &corev1.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ @@ -920,7 +746,7 @@ func getAffinity(cluster *redisv1alpha1.DistributedRedisCluster, labels map[stri }, }, } - case redisv1alpha1.AntiAffinity: + case core.AntiAffinity: return &corev1.Affinity{ PodAntiAffinity: &corev1.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ @@ -965,8 +791,8 @@ func getAffinity(cluster *redisv1alpha1.DistributedRedisCluster, labels map[stri } } -// IsRedisClusterStatefulsetChanged -func IsRedisClusterStatefulsetChanged(newSts, sts *appv1.StatefulSet, logger logr.Logger) bool { +// IsStatefulsetChanged +func IsStatefulsetChanged(newSts, sts *appv1.StatefulSet, logger logr.Logger) bool { // statefulset check if !reflect.DeepEqual(newSts.GetLabels(), sts.GetLabels()) || !reflect.DeepEqual(newSts.GetAnnotations(), sts.GetAnnotations()) { @@ -974,14 +800,14 @@ func IsRedisClusterStatefulsetChanged(newSts, sts *appv1.StatefulSet, logger log return true } - if *newSts.Spec.Replicas != *sts.Spec.Replicas { + if *newSts.Spec.Replicas != *sts.Spec.Replicas || + newSts.Spec.ServiceName != sts.Spec.ServiceName { logger.V(2).Info("replicas diff") return true } for _, name := range []string{ RedisStorageVolumeName, - RedisRestoreLocalVolumeName, } { oldPvc := util.GetVolumeClaimTemplatesByName(sts.Spec.VolumeClaimTemplates, name) newPvc := util.GetVolumeClaimTemplatesByName(newSts.Spec.VolumeClaimTemplates, name) @@ -1002,13 +828,6 @@ func IsRedisClusterStatefulsetChanged(newSts, sts *appv1.StatefulSet, logger log return IsPodTemplasteChanged(&newSts.Spec.Template, &sts.Spec.Template, logger) } -func pullPolicy(specPolicy corev1.PullPolicy) corev1.PullPolicy { - if specPolicy == "" { - return corev1.PullAlways - } - return specPolicy -} - func emptyVolume() *corev1.Volume { return &corev1.Volume{ Name: RedisStorageVolumeName, @@ -1018,11 +837,6 @@ func emptyVolume() *corev1.Volume { } } -func customContainerEnv(env []corev1.EnvVar, customEnv []corev1.EnvVar) []corev1.EnvVar { - env = append(env, customEnv...) - return env -} - func ClusterStatefulSetName(clusterName string, i int) string { return fmt.Sprintf("drc-%s-%d", clusterName, i) } @@ -1030,3 +844,17 @@ func ClusterStatefulSetName(clusterName string, i int) string { func ClusterHeadlessSvcName(name string, i int) string { return fmt.Sprintf("%s-%d", name, i) } + +func ParsePodShardAndIndex(name string) (shard int, index int, err error) { + fields := strings.Split(name, "-") + if len(fields) < 3 { + return -1, -1, fmt.Errorf("invalid pod name %s", name) + } + if index, err = strconv.Atoi(fields[len(fields)-1]); err != nil { + return -1, -1, fmt.Errorf("invalid pod name %s", name) + } + if shard, err = strconv.Atoi(fields[len(fields)-2]); err != nil { + return -1, -1, fmt.Errorf("invalid pod name %s", name) + } + return shard, index, nil +} diff --git a/internal/builder/clusterbuilder/status.go b/internal/builder/clusterbuilder/status.go new file mode 100644 index 0000000..d293fa9 --- /dev/null +++ b/internal/builder/clusterbuilder/status.go @@ -0,0 +1,91 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clusterbuilder + +import ( + "encoding/json" + "fmt" + "math" + "reflect" + "strconv" + "time" + + "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewRedisClusterDetailedStatusConfigMap creates a new ConfigMap for the given Cluster +func NewRedisClusterDetailedStatusConfigMap(inst types.RedisClusterInstance, status *v1alpha1.DistributedRedisClusterDetailedStatus) (*corev1.ConfigMap, error) { + data, _ := json.MarshalIndent(status, "", " ") + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("drc-%s-status", inst.GetName()), + Namespace: inst.GetNamespace(), + Labels: GetClusterLabels(inst.GetName(), nil), + Annotations: map[string]string{ + "updateTimestamp": fmt.Sprintf("%d", metav1.Now().Unix()), + }, + OwnerReferences: util.BuildOwnerReferencesWithParents(inst.Definition()), + }, + Data: map[string]string{"status": string(data)}, + }, nil +} + +func ShouldUpdateDetailedStatusConfigMap(cm *corev1.ConfigMap, status *v1alpha1.DistributedRedisClusterDetailedStatus) bool { + if cm.Data == nil || cm.Data["status"] == "" { + return true + } + + oldStatus := &v1alpha1.DistributedRedisClusterDetailedStatus{} + if err := json.Unmarshal([]byte(cm.Data["status"]), oldStatus); err != nil { + return true + } + if oldStatus.Status != status.Status || + oldStatus.Reason != status.Reason || + len(oldStatus.Nodes) != len(status.Nodes) || + oldStatus.NumberOfMaster != status.NumberOfMaster || + !reflect.DeepEqual(oldStatus.Shards, status.Shards) { + return true + } + + // if the last update is more than 5 minutes ago, we should update the status + tsVal := cm.Annotations["updateTimestamp"] + timestamp, _ := strconv.ParseInt(tsVal, 10, 64) + if timestamp+60*5 < time.Now().Unix() { + return true + } + + for i := 0; i < len(oldStatus.Nodes); i++ { + onode, nnode := oldStatus.Nodes[i], status.Nodes[i] + if onode.Role != nnode.Role || + onode.IP != nnode.IP || + onode.Port != nnode.Port || + onode.Version != nnode.Version || + onode.NodeName != nnode.NodeName || + + // if the memory usage is different than 1MB, we should update the status + math.Abs(float64(onode.UsedMemory-nnode.UsedMemory)) >= 1024*1024 || + math.Abs(float64(onode.UsedMemoryDataset-nnode.UsedMemoryDataset)) > 1024*1024 { + return true + } + } + return false +} diff --git a/pkg/util/cluster.go b/internal/builder/config.go similarity index 67% rename from pkg/util/cluster.go rename to internal/builder/config.go index dc61575..2ab10d1 100644 --- a/pkg/util/cluster.go +++ b/internal/builder/config.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,10 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package builder -import "fmt" +var MustQuoteRedisConfig = map[string]struct{}{ + "tls-protocols": {}, +} -func GenerateClusterACLConfigMapName(name string) string { - return fmt.Sprintf("drc-acl-%s", name) +var MustUpperRedisConfig = map[string]struct{}{ + "tls-ciphers": {}, + "tls-ciphersuites": {}, + "tls-protocols": {}, } diff --git a/internal/builder/const.go b/internal/builder/const.go new file mode 100644 index 0000000..16d2d81 --- /dev/null +++ b/internal/builder/const.go @@ -0,0 +1,38 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +const ( + LabelRedisArch = "redisarch" + ManagedByLabel = "managed-by" + InstanceTypeLabel = "middleware.instance/type" + InstanceNameLabel = "middleware.instance/name" + + PodNameLabelKey = "statefulset.kubernetes.io/pod-name" + PodAnnounceIPLabelKey = "middleware.alauda.io/announce_ip" + PodAnnouncePortLabelKey = "middleware.alauda.io/announce_port" + PodAnnounceIPortLabelKey = "middleware.alauda.io/announce_iport" + + AppLabel = "redis-failover" + HostnameTopologyKey = "kubernetes.io/hostname" + RestartAnnotationKey = "kubectl.kubernetes.io/restartedAt" + + ConfigSigAnnotationKey = "middleware.alauda.io/config-sig" + PasswordSigAnnotationKey = "middleware.alauda.io/secret-sig" + + S3SecretVolumeName = "s3-secret" +) diff --git a/internal/builder/failoverbuilder/acl.go b/internal/builder/failoverbuilder/acl.go new file mode 100644 index 0000000..e3d3010 --- /dev/null +++ b/internal/builder/failoverbuilder/acl.go @@ -0,0 +1,122 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failoverbuilder + +import ( + "fmt" + "strings" + + "github.com/alauda/redis-operator/api/core" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + midv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/util" + security "github.com/alauda/redis-operator/pkg/security/password" + "github.com/alauda/redis-operator/pkg/types/user" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GenerateFailoverACLConfigMapName(name string) string { + return fmt.Sprintf("rfr-acl-%s", name) +} + +// acl operator secret +func GenerateFailoverACLOperatorSecretName(name string) string { + return fmt.Sprintf("rfr-acl-%s-operator-secret", name) +} + +func NewFailoverOpSecret(rf *databasesv1.RedisFailover) *corev1.Secret { + randPassword, _ := security.GeneratePassword(security.MaxPasswordLen) + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: GenerateFailoverACLOperatorSecretName(rf.Name), + Namespace: rf.Namespace, + OwnerReferences: util.BuildOwnerReferences(rf), + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "password": []byte(randPassword), + "username": []byte("operator"), + }, + } +} + +func NewFailoverAclConfigMap(rf *databasesv1.RedisFailover, data map[string]string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: GenerateFailoverACLConfigMapName(rf.Name), + Namespace: rf.Namespace, + OwnerReferences: util.BuildOwnerReferences(rf), + }, + Data: data, + } +} + +func GenerateFailoverOperatorsRedisUserName(name string) string { + return fmt.Sprintf("rfr-acl-%s-operator", name) +} + +func GenerateFailoverDefaultRedisUserName(name string) string { + return fmt.Sprintf("rfr-acl-%s-default", name) +} + +func GenerateFailoverRedisUserName(instName, name string) string { + return fmt.Sprintf("rfr-acl-%s-%s", instName, name) +} + +func GenerateFailoverRedisUser(obj metav1.Object, u *user.User) *midv1.RedisUser { + var ( + name = GenerateFailoverRedisUserName(obj.GetName(), u.Name) + accountType midv1.AccountType + passwordSecrets []string + ) + switch u.Role { + case user.RoleOperator: + accountType = midv1.System + default: + if u.Name == "default" { + accountType = midv1.Default + } else { + accountType = midv1.Custom + } + } + if u.GetPassword().GetSecretName() != "" { + passwordSecrets = append(passwordSecrets, u.GetPassword().GetSecretName()) + } + var rules []string + for _, rule := range u.Rules { + rules = append(rules, rule.Encode()) + } + + return &midv1.RedisUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: obj.GetNamespace(), + Annotations: map[string]string{}, + Labels: map[string]string{}, + }, + Spec: midv1.RedisUserSpec{ + AccountType: accountType, + Arch: core.RedisSentinel, + RedisName: obj.GetName(), + Username: u.Name, + PasswordSecrets: passwordSecrets, + AclRules: strings.Join(rules, " "), + }, + } +} diff --git a/pkg/kubernetes/builder/sentinelbuilder/certificate.go b/internal/builder/failoverbuilder/certificate.go similarity index 59% rename from pkg/kubernetes/builder/sentinelbuilder/certificate.go rename to internal/builder/failoverbuilder/certificate.go index dad7171..f099498 100644 --- a/pkg/kubernetes/builder/sentinelbuilder/certificate.go +++ b/internal/builder/failoverbuilder/certificate.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,51 +14,39 @@ See the License for the specific language governing permissions and limitations under the License. */ -package sentinelbuilder +package failoverbuilder import ( - "fmt" "time" - v1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + "github.com/alauda/redis-operator/internal/util" certv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" v12 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// GenerateCertName -func GenerateCertName(name string) string { - return name + "-cert" -} - -func GetRedisSSLSecretName(name string) string { - return fmt.Sprintf("%s-tls", name) -} - -// GetServiceDNSName -func GetServiceDNSName(serviceName, namespace string) string { - return fmt.Sprintf("%s.%s.svc", serviceName, namespace) -} - // NewCertificate func NewCertificate(rf *v1.RedisFailover, selectors map[string]string) *certv1.Certificate { return &certv1.Certificate{ ObjectMeta: metav1.ObjectMeta{ - Name: GenerateCertName(rf.Name), + Name: builder.GenerateCertName(rf.Name), Namespace: rf.Namespace, Labels: GetCommonLabels(rf.Name, selectors), - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), + OwnerReferences: util.BuildOwnerReferences(rf), }, Spec: certv1.CertificateSpec{ // 10 year Duration: &metav1.Duration{Duration: 87600 * time.Hour}, DNSNames: []string{ - GetServiceDNSName(GetSentinelDeploymentName(rf.Name), rf.Namespace), - GetServiceDNSName(GetRedisROServiceName(rf.Name), rf.Namespace), - GetServiceDNSName(GetRedisRWServiceName(rf.Name), rf.Namespace), + builder.GetServiceDNSName(GetRedisROServiceName(rf.Name), rf.Namespace), + builder.GetServiceDNSName(GetRedisRWServiceName(rf.Name), rf.Namespace), + builder.GetServiceDNSName(sentinelbuilder.GetSentinelStatefulSetName(rf.Name), rf.Namespace), }, IssuerRef: v12.ObjectReference{Kind: certv1.ClusterIssuerKind, Name: "cpaas-ca"}, - SecretName: GetRedisSSLSecretName(rf.Name), + SecretName: builder.GetRedisSSLSecretName(rf.Name), }, } } diff --git a/internal/builder/failoverbuilder/configmap.go b/internal/builder/failoverbuilder/configmap.go new file mode 100644 index 0000000..99c5833 --- /dev/null +++ b/internal/builder/failoverbuilder/configmap.go @@ -0,0 +1,182 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failoverbuilder + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/alauda/redis-operator/api/core" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + RedisConfig_MaxMemory = "maxmemory" + RedisConfig_MaxMemoryPolicy = "maxmemory-policy" + RedisConfig_ClientOutputBufferLimit = "client-output-buffer-limit" + RedisConfig_Save = "save" + RedisConfig_RenameCommand = "rename-command" + RedisConfig_Appendonly = "appendonly" + RedisConfig_ReplDisklessSync = "repl-diskless-sync" +) + +func NewRedisConfigMap(st types.RedisFailoverInstance, selectors map[string]string) (*corev1.ConfigMap, error) { + rf := st.Definition() + customConfig := rf.Spec.Redis.CustomConfig + + default_config := make(map[string]string) + default_config["loglevel"] = "notice" + default_config["stop-writes-on-bgsave-error"] = "yes" + default_config["rdbcompression"] = "yes" + default_config["rdbchecksum"] = "yes" + default_config["slave-read-only"] = "yes" + default_config["repl-diskless-sync"] = "no" + default_config["slowlog-max-len"] = "128" + default_config["slowlog-log-slower-than"] = "10000" + default_config["maxclients"] = "10000" + default_config["hz"] = "10" + default_config["tcp-keepalive"] = "300" + default_config["tcp-backlog"] = "511" + default_config["protected-mode"] = "no" + + version, _ := redis.ParseRedisVersionFromImage(rf.Spec.Redis.Image) + innerRedisConfig := version.CustomConfigs(core.RedisSentinel) + default_config = lo.Assign(default_config, innerRedisConfig) + + for k, v := range customConfig { + k = strings.ToLower(k) + v = strings.TrimSpace(v) + if k == "save" && v == "60 100" { + continue + } + if k == RedisConfig_RenameCommand { + continue + } + default_config[k] = v + } + + // check if it's need to set default save + // check if aof enabled + if customConfig[RedisConfig_Appendonly] != "yes" && + customConfig[RedisConfig_ReplDisklessSync] != "yes" && + (customConfig[RedisConfig_Save] == "" || customConfig[RedisConfig_Save] == `""`) { + + default_config["save"] = "60 10000 300 100 600 1" + } + + if limits := rf.Spec.Redis.Resources.Limits; limits != nil { + if configedMem := customConfig[RedisConfig_MaxMemory]; configedMem == "" { + memLimit, _ := limits.Memory().AsInt64() + if policy := customConfig[RedisConfig_MaxMemoryPolicy]; policy == "noeviction" { + memLimit = int64(float64(memLimit) * 0.8) + } else { + memLimit = int64(float64(memLimit) * 0.7) + } + if memLimit > 0 { + default_config[RedisConfig_MaxMemory] = fmt.Sprintf("%d", memLimit) + } + } + } + + if !st.Version().IsACLSupported() { + var renameVal []string + if renameConfig, err := clusterbuilder.ParseRenameConfigs(customConfig[RedisConfig_RenameCommand]); err != nil { + return nil, err + } else { + for k, v := range renameConfig { + if k == "config" { + continue + } + renameVal = append(renameVal, k, v) + } + if len(renameVal) > 0 { + default_config[RedisConfig_RenameCommand] = strings.Join(renameVal, " ") + } + } + } + + keys := make([]string, 0, len(default_config)) + for k := range default_config { + keys = append(keys, k) + } + sort.Strings(keys) + + var buffer bytes.Buffer + for _, k := range keys { + v := default_config[k] + if v == "" || v == `""` { + buffer.WriteString(fmt.Sprintf("%s \"\"\n", k)) + continue + } + switch k { + case RedisConfig_ClientOutputBufferLimit: + fields := strings.Fields(v) + if len(fields)%4 != 0 { + continue + } + for i := 0; i < len(fields); i += 4 { + buffer.WriteString(fmt.Sprintf("%s %s %s %s %s\n", k, fields[i], fields[i+1], fields[i+2], fields[i+3])) + } + case RedisConfig_Save, RedisConfig_RenameCommand: + fields := strings.Fields(v) + if len(fields)%2 != 0 { + continue + } + for i := 0; i < len(fields); i += 2 { + buffer.WriteString(fmt.Sprintf("%s %s %s\n", k, fields[i], fields[i+1])) + } + default: + if _, ok := builder.MustQuoteRedisConfig[k]; ok && !strings.HasPrefix(v, `"`) { + v = fmt.Sprintf(`"%s"`, v) + } + if _, ok := builder.MustUpperRedisConfig[k]; ok { + v = strings.ToUpper(v) + } + buffer.WriteString(fmt.Sprintf("%s %s\n", k, v)) + } + } + + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetRedisConfigMapName(rf), + Namespace: rf.Namespace, + Labels: GetCommonLabels(rf.Name, selectors), + OwnerReferences: util.BuildOwnerReferences(rf), + }, + Data: map[string]string{ + RedisConfigFileName: buffer.String(), + }, + }, nil +} + +func GetRedisConfigMapName(rf *databasesv1.RedisFailover) string { + return GetFailoverStatefulSetName(rf.Name) +} + +func GetRedisScriptConfigMapName(name string) string { + return fmt.Sprintf("rfr-s-%s", name) +} diff --git a/internal/builder/failoverbuilder/helper.go b/internal/builder/failoverbuilder/helper.go new file mode 100644 index 0000000..e01897c --- /dev/null +++ b/internal/builder/failoverbuilder/helper.go @@ -0,0 +1,184 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failoverbuilder + +import ( + "fmt" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" +) + +const ( + BaseName = "rf" + SentinelName = "s" + SentinelRoleName = "sentinel" + SentinelConfigFileName = "sentinel.conf" + RedisConfigFileName = "redis.conf" + RedisName = "r" + RedisShutdownName = "r-s" + RedisReadinessName = "r-readiness" + RedisRoleName = "redis" + RedisMasterName = "mymaster" + + // TODO: reserverd for compatibility, remove in 3.22 + RedisSentinelSVCHostKey = "RFS_REDIS_SERVICE_HOST" + RedisSentinelSVCPortKey = "RFS_REDIS_SERVICE_PORT_SENTINEL" +) + +// variables refering to the redis exporter port +const ( + ExporterPort = 9121 + SentinelExporterPort = 9355 + SentinelPort = "26379" + ExporterPortName = "http-metrics" + RedisPort = 6379 + RedisPortString = "6379" + RedisPortName = "redis" + ExporterContainerName = "redis-exporter" + SentinelExporterContainerName = "sentinel-exporter" +) + +// label +const ( + LabelInstanceName = "app.kubernetes.io/name" + LabelPartOf = "app.kubernetes.io/part-of" + LabelRedisConfig = "redis.middleware.alauda.io/config" + LabelRedisConfigValue = "true" + LabelRedisRole = "redis.middleware.alauda.io/role" +) + +// Redis arch +const ( + Standalone = "standalone" + Sentinel = "sentinel" + Cluster = "cluster" +) + +// Redis role +const ( + Master = "master" + Slave = "slave" +) + +func GetCommonLabels(name string, extra ...map[string]string) map[string]string { + labels := getPublicLabels(name) + for _, item := range extra { + for k, v := range item { + labels[k] = v + } + } + return labels +} + +func getPublicLabels(name string) map[string]string { + return map[string]string{ + "redisfailovers.databases.spotahome.com/name": name, + "app.kubernetes.io/managed-by": "redis-operator", + builder.InstanceNameLabel: name, + builder.InstanceTypeLabel: "redis-failover", + } +} + +func GenerateSelectorLabels(component, name string) map[string]string { + return map[string]string{ + "app.kubernetes.io/part-of": "redis-failover", + "app.kubernetes.io/component": component, + "app.kubernetes.io/name": name, + } +} + +func GetFailoverStatefulSetName(sentinelName string) string { + return fmt.Sprintf("rfr-%s", sentinelName) +} + +// GetFailoverDeploymentName +// Deprecated in favor of standalone sentinel +func GetFailoverDeploymentName(sentinelName string) string { + return fmt.Sprintf("rfs-%s", sentinelName) +} + +// Redis standalone annotation +const ( + AnnotationStandaloneLoadFilePath = "redis-standalone/filepath" + AnnotationStandaloneInitStorage = "redis-standalone/storage-type" + AnnotationStandaloneInitPvcName = "redis-standalone/pvc-name" + AnnotationStandaloneInitHostPath = "redis-standalone/hostpath" +) + +func NeedStandaloneInit(rf *v1.RedisFailover) bool { + if rf.Annotations[AnnotationStandaloneInitStorage] != "" && + (rf.Annotations[AnnotationStandaloneInitPvcName] != "" || rf.Annotations[AnnotationStandaloneInitHostPath] != "") && + rf.Annotations[AnnotationStandaloneLoadFilePath] != "" { + return true + } + return false +} + +func GenerateName(typeName, metaName string) string { + return fmt.Sprintf("%s%s-%s", BaseName, typeName, metaName) +} + +func GetRedisName(rf *v1.RedisFailover) string { + return GenerateName(RedisName, rf.Name) +} + +func GetFailoverNodePortServiceName(rf *v1.RedisFailover, index int) string { + name := GetFailoverStatefulSetName(rf.Name) + return fmt.Sprintf("%s-%d", name, index) +} + +func GetRedisShutdownName(rf *v1.RedisFailover) string { + return GenerateName(RedisShutdownName, rf.Name) +} + +func GetRedisNameExporter(rf *v1.RedisFailover) string { + return GenerateName(fmt.Sprintf("%s%s", RedisName, "e"), rf.Name) +} + +func GetRedisNodePortSvc(rf *v1.RedisFailover) string { + return GenerateName(fmt.Sprintf("%s-%s", RedisName, "n"), rf.Name) +} + +func GetRedisSecretName(rf *v1.RedisFailover) string { + return GenerateName(fmt.Sprintf("%s-%s", RedisName, "p"), rf.Name) +} + +func GetRedisShutdownConfigMapName(rf *v1.RedisFailover) string { + return GenerateName(fmt.Sprintf("%s-%s", RedisName, "s"), rf.Name) +} + +func GetCronJobName(redisName, scheduleName string) string { + return fmt.Sprintf("%s-%s", redisName, scheduleName) +} + +// Sentinel +func GetSentinelReadinessConfigmap(name string) string { + return GenerateName(fmt.Sprintf("%s-%s", SentinelName, "r"), name) +} + +func GetSentinelConfigmap(name string) string { + return GenerateName(fmt.Sprintf("%s-%s", SentinelName, "r"), name) +} + +func GetSentinelName(name string) string { + return GenerateName(SentinelName, name) +} + +func GetSentinelHeadlessSvc(name string) string { + return GenerateName(SentinelName, fmt.Sprintf("%s-%s", name, "hl")) +} diff --git a/internal/builder/failoverbuilder/poddisruptionbudget.go b/internal/builder/failoverbuilder/poddisruptionbudget.go new file mode 100644 index 0000000..cd028f1 --- /dev/null +++ b/internal/builder/failoverbuilder/poddisruptionbudget.go @@ -0,0 +1,49 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failoverbuilder + +import ( + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/util" + "github.com/samber/lo" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func NewPodDisruptionBudgetForCR(rf *databasesv1.RedisFailover, selectors map[string]string) *policyv1.PodDisruptionBudget { + maxUnavailable := intstr.FromInt(int(rf.Spec.Redis.Replicas) - 1) + namespace := rf.Namespace + selectors = lo.Assign(selectors, GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) + labels := lo.Assign(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleRedis, rf.Name), selectors) + + name := GetFailoverStatefulSetName(rf.Name) + return &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Name: name, + Namespace: namespace, + OwnerReferences: util.BuildOwnerReferences(rf), + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: &maxUnavailable, + Selector: &metav1.LabelSelector{ + MatchLabels: selectors, + }, + }, + } +} diff --git a/internal/builder/failoverbuilder/sentinel.go b/internal/builder/failoverbuilder/sentinel.go new file mode 100644 index 0000000..2914e55 --- /dev/null +++ b/internal/builder/failoverbuilder/sentinel.go @@ -0,0 +1,58 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failoverbuilder + +import ( + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types" + + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewFailoverSentinel(inst types.RedisFailoverInstance) *v1.RedisSentinel { + def := inst.Definition() + + sen := &v1.RedisSentinel{ + ObjectMeta: metav1.ObjectMeta{ + Name: def.Name, + Namespace: def.Namespace, + Labels: def.Labels, + Annotations: def.Annotations, + OwnerReferences: util.BuildOwnerReferences(def), + }, + Spec: def.Spec.Sentinel.RedisSentinelSpec, + } + + sen.Spec.Image = lo.FirstOrEmpty([]string{def.Spec.Sentinel.Image, def.Spec.Redis.Image}) + sen.Spec.ImagePullPolicy = builder.GetPullPolicy(def.Spec.Sentinel.ImagePullPolicy, def.Spec.Redis.ImagePullPolicy) + sen.Spec.ImagePullSecrets = lo.FirstOrEmpty([][]corev1.LocalObjectReference{ + def.Spec.Sentinel.ImagePullSecrets, + def.Spec.Redis.ImagePullSecrets, + }) + sen.Spec.Replicas = def.Spec.Sentinel.Replicas + sen.Spec.Resources = def.Spec.Sentinel.Resources + sen.Spec.CustomConfig = def.Spec.Sentinel.CustomConfig + sen.Spec.Exporter = def.Spec.Sentinel.Exporter + if sen.Spec.EnableTLS { + sen.Spec.ExternalTLSSecret = builder.GetRedisSSLSecretName(inst.GetName()) + } + return sen +} diff --git a/pkg/kubernetes/builder/sentinelbuilder/service.go b/internal/builder/failoverbuilder/service.go similarity index 57% rename from pkg/kubernetes/builder/sentinelbuilder/service.go rename to internal/builder/failoverbuilder/service.go index 7e4fc64..b1e947d 100644 --- a/pkg/kubernetes/builder/sentinelbuilder/service.go +++ b/internal/builder/failoverbuilder/service.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,11 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package sentinelbuilder +package failoverbuilder import ( - v1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/util" + "strconv" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" + "github.com/samber/lo" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -28,7 +32,7 @@ const ( RedisArchRoleRedis = "redis" RedisArchRoleSEN = "sentinel" RedisRoleMaster = "master" - RedisRoleSlave = "slave" + RedisRoleReplica = "slave" RedisRoleLabel = "redis.middleware.alauda.io/role" RedisSVCPort = 6379 RedisSVCPortName = "redis" @@ -41,7 +45,7 @@ func NewRWSvcForCR(rf *v1.RedisFailover) *corev1.Service { svcName := GetRedisRWServiceName(rf.Name) ptype := corev1.IPFamilyPolicySingleStack protocol := []corev1.IPFamily{} - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { + if rf.Spec.Redis.Expose.IPFamilyPrefer == corev1.IPv6Protocol { protocol = append(protocol, corev1.IPv6Protocol) } else { protocol = append(protocol, corev1.IPv4Protocol) @@ -51,13 +55,13 @@ func NewRWSvcForCR(rf *v1.RedisFailover) *corev1.Service { Name: svcName, Namespace: rf.Namespace, Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), + OwnerReferences: util.BuildOwnerReferences(rf), Annotations: rf.Spec.Redis.ServiceAnnotations, }, Spec: corev1.ServiceSpec{ + Type: rf.Spec.Redis.Expose.ServiceType, IPFamilies: protocol, IPFamilyPolicy: &ptype, - Type: corev1.ServiceTypeClusterIP, Ports: []corev1.ServicePort{ { Port: RedisSVCPort, @@ -73,12 +77,12 @@ func NewRWSvcForCR(rf *v1.RedisFailover) *corev1.Service { func NewReadOnlyForCR(rf *v1.RedisFailover) *corev1.Service { selectorLabels := GenerateSelectorLabels(RedisArchRoleRedis, rf.Name) - selectorLabels[RedisRoleLabel] = RedisRoleSlave + selectorLabels[RedisRoleLabel] = RedisRoleReplica labels := GetCommonLabels(rf.Name, selectorLabels) svcName := GetRedisROServiceName(rf.Name) ptype := corev1.IPFamilyPolicySingleStack protocol := []corev1.IPFamily{} - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { + if rf.Spec.Redis.Expose.IPFamilyPrefer == corev1.IPv6Protocol { protocol = append(protocol, corev1.IPv6Protocol) } else { protocol = append(protocol, corev1.IPv4Protocol) @@ -88,13 +92,13 @@ func NewReadOnlyForCR(rf *v1.RedisFailover) *corev1.Service { Name: svcName, Namespace: rf.Namespace, Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), + OwnerReferences: util.BuildOwnerReferences(rf), Annotations: rf.Spec.Redis.ServiceAnnotations, }, Spec: corev1.ServiceSpec{ + Type: rf.Spec.Redis.Expose.ServiceType, IPFamilies: protocol, IPFamilyPolicy: &ptype, - Type: corev1.ServiceTypeClusterIP, Ports: []corev1.ServicePort{ { Port: RedisSVCPort, @@ -108,90 +112,21 @@ func NewReadOnlyForCR(rf *v1.RedisFailover) *corev1.Service { } } -func NewSentinelServiceForCR(rf *v1.RedisFailover, selectors map[string]string) *corev1.Service { - name := GetSentinelServiceName(rf.Name) - namespace := rf.Namespace - sentinelTargetPort := intstr.FromInt(26379) - - selectorLabels := MergeMap(GenerateSelectorLabels(RedisArchRoleSEN, rf.Name), GetCommonLabels(rf.Name)) - if len(selectors) > 0 { - selectorLabels = MergeMap(selectors, GenerateSelectorLabels(RedisArchRoleSEN, rf.Name)) - } - labels := GetCommonLabels(rf.Name, GenerateSelectorLabels(RedisArchRoleSEN, rf.Name), selectorLabels) - - ptype := corev1.IPFamilyPolicySingleStack - protocol := []corev1.IPFamily{} - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - protocol = append(protocol, corev1.IPv6Protocol) - } else { - protocol = append(protocol, corev1.IPv4Protocol) - } - - svc := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Spec: corev1.ServiceSpec{ - IPFamilies: protocol, - IPFamilyPolicy: &ptype, - Selector: selectorLabels, - Ports: []corev1.ServicePort{ - { - Name: "sentinel", - Port: 26379, - TargetPort: sentinelTargetPort, - Protocol: "TCP", - }, - }, - }, - } - - if rf.Spec.Expose.EnableNodePort { - svc = &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Spec: corev1.ServiceSpec{ - IPFamilyPolicy: &ptype, - IPFamilies: protocol, - Selector: selectorLabels, - Type: corev1.ServiceTypeNodePort, - Ports: []corev1.ServicePort{ - { - Name: "sentinel", - Port: 26379, - TargetPort: sentinelTargetPort, - Protocol: "TCP", - NodePort: rf.Spec.Expose.AccessPort, - }, - }, - }, - } - } - return svc -} - func NewExporterServiceForCR(rf *v1.RedisFailover, selectors map[string]string) *corev1.Service { - name := GetSentinelStatefulSetName(rf.Name) + name := GetFailoverStatefulSetName(rf.Name) namespace := rf.Namespace selectorLabels := GenerateSelectorLabels(RedisArchRoleRedis, rf.Name) labels := GetCommonLabels(rf.Name, selectors, selectorLabels) - labels[LabelRedisArch] = RedisArchRoleSEN + labels[builder.LabelRedisArch] = RedisArchRoleSEN defaultAnnotations := map[string]string{ "prometheus.io/scrape": "true", "prometheus.io/port": "http", "prometheus.io/path": "/metrics", } - annotations := MergeMap(defaultAnnotations, rf.Spec.Redis.ServiceAnnotations) + annotations := lo.Assign(defaultAnnotations, rf.Spec.Redis.ServiceAnnotations) ptype := corev1.IPFamilyPolicySingleStack protocol := []corev1.IPFamily{} - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { + if rf.Spec.Redis.Expose.IPFamilyPrefer == corev1.IPv6Protocol { protocol = append(protocol, corev1.IPv6Protocol) } else { protocol = append(protocol, corev1.IPv4Protocol) @@ -203,7 +138,7 @@ func NewExporterServiceForCR(rf *v1.RedisFailover, selectors map[string]string) Namespace: namespace, Labels: labels, Annotations: annotations, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), + OwnerReferences: util.BuildOwnerReferences(rf), }, Spec: corev1.ServiceSpec{ IPFamilies: protocol, @@ -223,38 +158,43 @@ func NewExporterServiceForCR(rf *v1.RedisFailover, selectors map[string]string) } } -func NewRedisNodePortService(rf *v1.RedisFailover, index string, nodePort int32, selectors map[string]string) *corev1.Service { +// NewPodService returns a new Service for the given RedisFailover and index, with the configed service type +func NewPodService(rf *v1.RedisFailover, index int, selectors map[string]string) *corev1.Service { + return NewPodNodePortService(rf, index, selectors, 0) +} + +func NewPodNodePortService(rf *v1.RedisFailover, index int, selectors map[string]string, nodePort int32) *corev1.Service { namespace := rf.Namespace ptype := corev1.IPFamilyPolicySingleStack protocol := []corev1.IPFamily{} - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { + if rf.Spec.Redis.Expose.IPFamilyPrefer == corev1.IPv6Protocol { protocol = append(protocol, corev1.IPv6Protocol) } else { protocol = append(protocol, corev1.IPv4Protocol) } - _labels := util.MergeMap(GetCommonLabels(rf.Name), selectors, GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) + labels := lo.Assign(GetCommonLabels(rf.Name), selectors, GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) selectorLabels := map[string]string{ - "statefulset.kubernetes.io/pod-name": GetSentinelStatefulSetName(rf.Name) + "-" + index, + builder.PodNameLabelKey: GetFailoverStatefulSetName(rf.Name) + "-" + strconv.Itoa(index), } - redisTargetPort := intstr.FromInt(6379) return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: GetSentinelStatefulSetName(rf.Name) + "-" + index, + Name: GetFailoverNodePortServiceName(rf, index), Namespace: namespace, - Labels: _labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), + Labels: labels, + Annotations: rf.Spec.Redis.Expose.Annotations, + OwnerReferences: util.BuildOwnerReferences(rf), }, Spec: corev1.ServiceSpec{ + Type: rf.Spec.Redis.Expose.ServiceType, IPFamilies: protocol, IPFamilyPolicy: &ptype, - Type: corev1.ServiceTypeNodePort, Ports: []corev1.ServicePort{ { Port: 6379, Protocol: corev1.ProtocolTCP, Name: "client", - TargetPort: redisTargetPort, + TargetPort: intstr.FromInt(6379), NodePort: nodePort, }, }, diff --git a/pkg/kubernetes/builder/sentinelbuilder/servicemonitor.go b/internal/builder/failoverbuilder/servicemonitor.go similarity index 73% rename from pkg/kubernetes/builder/sentinelbuilder/servicemonitor.go rename to internal/builder/failoverbuilder/servicemonitor.go index 4c40412..bd8bd67 100644 --- a/pkg/kubernetes/builder/sentinelbuilder/servicemonitor.go +++ b/internal/builder/failoverbuilder/servicemonitor.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,93 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package sentinelbuilder +package failoverbuilder import ( - "fmt" - "os" - "strings" - - v1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/util" + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" smv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" ) -func NewServiceMonitorForCR(rf *v1.RedisFailover, sentnelLabels map[string]string) *smv1.ServiceMonitor { - interval := "60s" - scrapeTimeout := "10s" - configs := []*smv1.RelabelConfig{{ - Action: "keep", - Regex: getDefaultRegex(regexArr), - SourceLabels: []smv1.LabelName{"__name__"}, - }} - - if rf != nil { - if rf.Spec.ServiceMonitor.Interval != "" { - interval = rf.Spec.ServiceMonitor.Interval - } - if rf.Spec.ServiceMonitor.ScrapeTimeout != "" { - scrapeTimeout = rf.Spec.ServiceMonitor.ScrapeTimeout - } - if rf.Spec.ServiceMonitor.CustomMetricRelabelings { - configs = rf.Spec.ServiceMonitor.MetricRelabelConfigs - } - } - - sm := &smv1.ServiceMonitor{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-sentinel", - Labels: map[string]string{ - "prometheus": "kube-prometheus", - }, - }, - Spec: smv1.ServiceMonitorSpec{ - Selector: metav1.LabelSelector{ - MatchLabels: sentnelLabels, - }, - NamespaceSelector: smv1.NamespaceSelector{ - Any: true, - }, - Endpoints: []smv1.Endpoint{ - // for sentinel metrics - { - HonorLabels: true, - Port: "http-metrics", - Path: "/metrics", - Interval: smv1.Duration(interval), - ScrapeTimeout: smv1.Duration(scrapeTimeout), - MetricRelabelConfigs: configs, - }, - { - HonorLabels: true, - Port: "metrics", - Path: "/metrics", - Interval: smv1.Duration(interval), - ScrapeTimeout: smv1.Duration(scrapeTimeout), - MetricRelabelConfigs: configs, - }, - // for cluster metrics - { - HonorLabels: true, - Port: "prom-http", - Path: "/metrics", - Interval: smv1.Duration(interval), - ScrapeTimeout: smv1.Duration(scrapeTimeout), - MetricRelabelConfigs: configs, - }, - }, - TargetLabels: []string{util.LabelRedisArch}, - }, - } - - return sm - -} - var regexArr = []string{ "redis_instance_info", + "redis_master_link_up", "redis_slave_info", "redis_connected_clients", "redis_uptime_in_seconds", @@ -154,27 +79,68 @@ var regexArr = []string{ "redis_config_maxmemory", } -func getDefaultRegex(regex []string) string { - return fmt.Sprintf("(%s)", strings.Join(uniqueArr(regex), "|")) -} +const ( + DefaultScrapInterval = "60s" + DefaultScrapeTimeout = "10s" +) -func uniqueArr(m []string) []string { - d := make([]string, 0) - result := make(map[string]bool, len(m)) - for _, v := range m { - if !result[v] { - result[v] = true - d = append(d, v) - } +func NewServiceMonitorForCR(rf *v1.RedisFailover) *smv1.ServiceMonitor { + sentinelLabels := map[string]string{ + "app.kubernetes.io/part-of": "redis-failover", } - return d -} -func GetPodOwnerReferences() metav1.OwnerReference { - return metav1.OwnerReference{ - APIVersion: "v1", - Kind: "Pod", - Name: os.Getenv("POD_NAME"), - UID: types.UID(os.Getenv("POD_UID")), + interval := "60s" + scrapeTimeout := "10s" + configs := []*smv1.RelabelConfig{{ + Action: "keep", + Regex: builder.BuildMetricsRegex(regexArr), + SourceLabels: []smv1.LabelName{"__name__"}, + }} + + sm := &smv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-sentinel", + Labels: map[string]string{ + "prometheus": "kube-prometheus", + }, + }, + Spec: smv1.ServiceMonitorSpec{ + Selector: metav1.LabelSelector{ + MatchLabels: sentinelLabels, + }, + NamespaceSelector: smv1.NamespaceSelector{ + Any: true, + }, + Endpoints: []smv1.Endpoint{ + // for sentinel metrics + { + HonorLabels: true, + Port: "http-metrics", + Path: "/metrics", + Interval: smv1.Duration(interval), + ScrapeTimeout: smv1.Duration(scrapeTimeout), + MetricRelabelConfigs: configs, + }, + { + HonorLabels: true, + Port: "metrics", + Path: "/metrics", + Interval: smv1.Duration(interval), + ScrapeTimeout: smv1.Duration(scrapeTimeout), + MetricRelabelConfigs: configs, + }, + // for cluster metrics + { + HonorLabels: true, + Port: "prom-http", + Path: "/metrics", + Interval: smv1.Duration(interval), + ScrapeTimeout: smv1.Duration(scrapeTimeout), + MetricRelabelConfigs: configs, + }, + }, + TargetLabels: []string{builder.LabelRedisArch}, + }, } + return sm } diff --git a/internal/builder/failoverbuilder/statefulset.go b/internal/builder/failoverbuilder/statefulset.go new file mode 100644 index 0000000..76db8af --- /dev/null +++ b/internal/builder/failoverbuilder/statefulset.go @@ -0,0 +1,757 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failoverbuilder + +import ( + "fmt" + "net" + "path" + "strconv" + "strings" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/user" + "github.com/samber/lo" + appv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" +) + +const ( + redisStorageVolumeName = "redis-data" + exporterContainerName = "redis-exporter" + graceTime = 30 + PasswordENV = "REDIS_PASSWORD" + redisConfigurationVolumeName = "redis-config" + RedisTmpVolumeName = "redis-tmp" + RedisTLSVolumeName = "redis-tls" + redisAuthName = "redis-auth" + redisStandaloneVolumeName = "redis-standalone" + redisOptName = "redis-opt" + OperatorUsername = "OPERATOR_USERNAME" + OperatorSecretName = "OPERATOR_SECRET_NAME" + MonitorOperatorSecretName = "MONITOR_OPERATOR_SECRET_NAME" + ServerContainerName = "redis" +) + +func GetRedisRWServiceName(failoverName string) string { + return fmt.Sprintf("rfr-%s-read-write", failoverName) +} + +func GetRedisROServiceName(failoverName string) string { + return fmt.Sprintf("rfr-%s-read-only", failoverName) +} + +func GenerateRedisStatefulSet(inst types.RedisFailoverInstance, selectors map[string]string, isAllACLSupported bool) *appv1.StatefulSet { + + var ( + rf = inst.Definition() + + users = inst.Users() + opUser = users.GetOpUser() + aclConfigMapName = GenerateFailoverACLConfigMapName(rf.Name) + ) + if opUser.Role == user.RoleOperator && !isAllACLSupported { + opUser = users.GetDefaultUser() + } + if !inst.Version().IsACLSupported() { + aclConfigMapName = "" + } + + if len(selectors) == 0 { + selectors = lo.Assign(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) + } else { + selectors = lo.Assign(selectors, GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) + } + labels := lo.Assign(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleRedis, rf.Name), selectors) + + secretName := rf.Spec.Auth.SecretPath + if opUser.GetPassword() != nil { + secretName = opUser.GetPassword().SecretName + } + redisCommand := getRedisCommand(rf) + volumeMounts := getRedisVolumeMounts(rf, secretName) + volumes := getRedisVolumes(inst, rf, secretName) + + localhost := "127.0.0.1" + if rf.Spec.Redis.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + localhost = "::1" + } + ss := &appv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetFailoverStatefulSetName(rf.Name), + Namespace: rf.Namespace, + Labels: labels, + OwnerReferences: util.BuildOwnerReferences(rf), + }, + Spec: appv1.StatefulSetSpec{ + ServiceName: GetFailoverStatefulSetName(rf.Name), + Replicas: &rf.Spec.Redis.Replicas, + PodManagementPolicy: appv1.ParallelPodManagement, + UpdateStrategy: appv1.StatefulSetUpdateStrategy{ + Type: appv1.RollingUpdateStatefulSetStrategyType, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: selectors, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: rf.Spec.Redis.PodAnnotations, + }, + Spec: corev1.PodSpec{ + HostAliases: []corev1.HostAlias{ + {IP: localhost, Hostnames: []string{config.LocalInjectName}}, + }, + ServiceAccountName: clusterbuilder.RedisInstanceServiceAccountName, + Affinity: getAffinity(rf.Spec.Redis.Affinity, selectors), + Tolerations: rf.Spec.Redis.Tolerations, + NodeSelector: rf.Spec.Redis.NodeSelector, + SecurityContext: builder.GetPodSecurityContext(rf.Spec.Redis.SecurityContext), + ImagePullSecrets: rf.Spec.Redis.ImagePullSecrets, + TerminationGracePeriodSeconds: pointer.Int64(clusterbuilder.DefaultTerminationGracePeriodSeconds), + Containers: []corev1.Container{ + { + Name: "redis", + Image: rf.Spec.Redis.Image, + ImagePullPolicy: builder.GetPullPolicy(rf.Spec.Redis.ImagePullPolicy), + Env: createRedisContainerEnvs(inst, opUser, aclConfigMapName), + Ports: []corev1.ContainerPort{ + { + Name: "redis", + ContainerPort: 6379, + Protocol: corev1.ProtocolTCP, + }, + }, + VolumeMounts: volumeMounts, + Command: redisCommand, + StartupProbe: &corev1.Probe{ + InitialDelaySeconds: graceTime, + TimeoutSeconds: 5, + FailureThreshold: 10, + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(6379), + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + InitialDelaySeconds: 1, + TimeoutSeconds: 5, + FailureThreshold: 5, + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(6379), + }, + }, + }, + LivenessProbe: &corev1.Probe{ + InitialDelaySeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 5, + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(6379), + }, + }, + }, + Resources: rf.Spec.Redis.Resources, + Lifecycle: &corev1.Lifecycle{ + PreStop: &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"/bin/sh", "-c", "/opt/redis-tools failover shutdown &> /proc/1/fd/1"}, + }, + }, + }, + SecurityContext: builder.GetSecurityContext(rf.Spec.Redis.SecurityContext), + }, + }, + InitContainers: []corev1.Container{ + createExposeContainer(rf), + }, + Volumes: volumes, + }, + }, + }, + } + + if rf.Spec.Redis.Storage.PersistentVolumeClaim != nil { + pvc := rf.Spec.Redis.Storage.PersistentVolumeClaim.DeepCopy() + if len(pvc.Spec.AccessModes) == 0 { + pvc.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce} + } + if !rf.Spec.Redis.Storage.KeepAfterDeletion { + // Set an owner reference so the persistent volumes are deleted when the rc is + pvc.OwnerReferences = util.BuildOwnerReferences(rf) + } + if pvc.Name == "" { + pvc.Name = getRedisDataVolumeName(rf) + } + ss.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{*pvc} + } + + if inst.IsStandalone() && NeedStandaloneInit(rf) { + ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, createStandaloneInitContainer(rf)) + } + + if rf.Spec.Redis.Exporter.Enabled { + defaultAnnotations := map[string]string{ + "prometheus.io/scrape": "true", + "prometheus.io/port": "http", + "prometheus.io/path": "/metrics", + } + ss.Spec.Template.Annotations = lo.Assign(ss.Spec.Template.Annotations, defaultAnnotations) + + exporter := createRedisExporterContainer(rf, opUser) + ss.Spec.Template.Spec.Containers = append(ss.Spec.Template.Spec.Containers, exporter) + } + return ss +} + +func createExposeContainer(rf *v1.RedisFailover) corev1.Container { + image := config.GetRedisToolsImage(rf) + container := corev1.Container{ + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + }, + Name: "init", + Image: image, + ImagePullPolicy: builder.GetPullPolicy(rf.Spec.Redis.ImagePullPolicy), + VolumeMounts: []corev1.VolumeMount{ + { + Name: redisOptName, + MountPath: "/mnt/opt/", + }, + { + Name: getRedisDataVolumeName(rf), + MountPath: "/data", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: "NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + { + Name: "SENTINEL_ANNOUNCE_PATH", + Value: "/data/announce.conf", + }, + { + Name: "IP_FAMILY_PREFER", + Value: string(rf.Spec.Redis.Expose.IPFamilyPrefer), + }, + { + Name: "SERVICE_TYPE", + Value: string(rf.Spec.Redis.Expose.ServiceType), + }, + }, + Command: []string{"sh", "/opt/init_failover.sh"}, + SecurityContext: builder.GetSecurityContext(rf.Spec.Redis.SecurityContext), + } + return container +} + +func createRedisContainerEnvs(inst types.RedisFailoverInstance, opUser *user.User, aclConfigMapName string) []corev1.EnvVar { + rf := inst.Definition() + + var monitorUri string + if inst.Monitor().Policy() == v1.SentinelFailoverPolicy { + var sentinelNodes []string + for _, node := range rf.Status.Monitor.Nodes { + sentinelNodes = append(sentinelNodes, net.JoinHostPort(node.IP, strconv.Itoa(int(node.Port)))) + } + monitorUri = fmt.Sprintf("sentinel://%s", strings.Join(sentinelNodes, ",")) + } + + redisEnvs := []corev1.EnvVar{ + { + Name: "NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + { + Name: "POD_UID", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.uid", + }, + }, + }, + { + Name: "POD_IP", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }, + { + Name: "POD_IPS", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIPs", + }, + }, + }, + { + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: "SERVICE_NAME", + Value: GetFailoverStatefulSetName(rf.Name), + }, + { + Name: "ACL_ENABLED", + // isAllACLSupported used to make sure the account is consistent with eachother + Value: fmt.Sprintf("%t", opUser.Role == user.RoleOperator), + }, + { + Name: "ACL_CONFIGMAP_NAME", + Value: aclConfigMapName, + }, + { + Name: OperatorUsername, + Value: opUser.Name, + }, + { + Name: OperatorSecretName, + Value: opUser.GetPassword().GetSecretName(), + }, + { + Name: "TLS_ENABLED", + Value: fmt.Sprintf("%t", rf.Spec.Redis.EnableTLS), + }, + { + Name: "PERSISTENT_ENABLED", + Value: fmt.Sprintf("%t", rf.Spec.Redis.Storage.PersistentVolumeClaim != nil), + }, + { + Name: "SERVICE_TYPE", + Value: string(rf.Spec.Redis.Expose.ServiceType), + }, + { + Name: "IP_FAMILY_PREFER", + Value: string(rf.Spec.Redis.Expose.IPFamilyPrefer), + }, + { + Name: "MONITOR_POLICY", + Value: string(inst.Monitor().Policy()), + }, + { + Name: "MONITOR_URI", + Value: monitorUri, + }, + { + Name: MonitorOperatorSecretName, + Value: rf.Status.Monitor.PasswordSecret, + }, + } + return redisEnvs +} + +func createStandaloneInitContainer(rf *v1.RedisFailover) corev1.Container { + tmpPath := "/tmp-data" + filepath := rf.Annotations[AnnotationStandaloneLoadFilePath] + targetFile := "" + if strings.HasSuffix(filepath, ".rdb") { + targetFile = "/data/dump.rdb" + } else { + targetFile = "/data/appendonly.aof" + } + + filepath = path.Join(tmpPath, filepath) + command := fmt.Sprintf("if [ -e '%s' ]; then", targetFile) + "\n" + + "echo 'redis storage file exist,skip' \n" + + "else \n" + + fmt.Sprintf("echo 'copy redis storage file' && cp %s %s ", filepath, targetFile) + + fmt.Sprintf("&& chown 999:1000 %s ", targetFile) + + fmt.Sprintf("&& chmod 644 %s \n", targetFile) + + "fi" + + container := corev1.Container{ + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, + }, + Name: "standalone-pod", + Image: config.GetRedisToolsImage(rf), + ImagePullPolicy: builder.GetPullPolicy(rf.Spec.Redis.ImagePullPolicy), + VolumeMounts: []corev1.VolumeMount{ + { + Name: getRedisDataVolumeName(rf), + MountPath: "/data", + }, + { + Name: redisStandaloneVolumeName, + MountPath: tmpPath, + }, + }, + Command: []string{"sh", "-c", command}, + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(false), + }, + } + + if rf.Annotations[AnnotationStandaloneInitStorage] == "hostpath" { + container.SecurityContext = &corev1.SecurityContext{ + Privileged: pointer.Bool(false), + RunAsGroup: pointer.Int64(0), + RunAsUser: pointer.Int64(0), + RunAsNonRoot: pointer.Bool(false), + } + } + return container +} + +func createRedisExporterContainer(rf *v1.RedisFailover, opUser *user.User) corev1.Container { + var ( + username string + secret string + ) + if opUser != nil { + username = opUser.Name + secret = opUser.GetPassword().GetSecretName() + if username == "default" { + username = "" + } + } + + container := corev1.Container{ + Name: exporterContainerName, + Image: rf.Spec.Redis.Exporter.Image, + ImagePullPolicy: builder.GetPullPolicy(rf.Spec.Redis.Exporter.ImagePullPolicy), + Env: []corev1.EnvVar{ + { + Name: "REDIS_ALIAS", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: "REDIS_USER", + Value: username, + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "metrics", + ContainerPort: 9121, + Protocol: corev1.ProtocolTCP, + }, + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, + }, + SecurityContext: &corev1.SecurityContext{ + ReadOnlyRootFilesystem: pointer.Bool(true), + }, + } + + if secret != "" { + //挂载 rf.Spec.Auth.SecretPath 到account + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: redisAuthName, + MountPath: "/account", + }) + } + + local_host := "127.0.0.1" + if rf.Spec.Redis.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + local_host = "[::1]" + } + if rf.Spec.Redis.EnableTLS { + container.VolumeMounts = []corev1.VolumeMount{ + { + Name: RedisTLSVolumeName, + MountPath: "/tls", + }, + } + container.Env = append(container.Env, []corev1.EnvVar{ + { + Name: "REDIS_EXPORTER_TLS_CLIENT_KEY_FILE", + Value: "/tls/tls.key", + }, + { + Name: "REDIS_EXPORTER_TLS_CLIENT_CERT_FILE", + Value: "/tls/tls.crt", + }, + { + Name: "REDIS_EXPORTER_TLS_CA_CERT_FILE", + Value: "/tls/ca.crt", + }, + { + Name: "REDIS_EXPORTER_SKIP_TLS_VERIFICATION", + Value: "true", + }, + { + Name: "REDIS_ADDR", + Value: fmt.Sprintf("redis://%s:6379", local_host), + }, + }...) + } else if rf.Spec.Redis.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + container.Env = append(container.Env, []corev1.EnvVar{ + {Name: "REDIS_ADDR", + Value: fmt.Sprintf("redis://%s:6379", local_host)}, + }...) + } + return container +} + +func getRedisCommand(rf *v1.RedisFailover) []string { + cmds := []string{"sh", "/opt/run_failover.sh"} + if rf.Spec.EnableActiveRedis { + cmds = append(cmds, "--loadmodule", "/modules/activeredis.so", + "service_id", fmt.Sprintf("%d", *rf.Spec.ServiceID), + "service_uid", string(rf.UID), + "shard_id", "0", + ) + } + return cmds +} + +func getAffinity(affinity *corev1.Affinity, labels map[string]string) *corev1.Affinity { + if affinity != nil { + return affinity + } + + // Return a SOFT anti-affinity + return &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ + { + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + TopologyKey: builder.HostnameTopologyKey, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + }, + }, + }, + }, + } +} + +func getRedisVolumeMounts(rf *v1.RedisFailover, secret string) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{ + { + Name: redisConfigurationVolumeName, + MountPath: "/redis", + }, + { + Name: getRedisDataVolumeName(rf), + MountPath: "/data", + }, + { + Name: RedisTmpVolumeName, + MountPath: "/tmp", + }, + { + Name: redisOptName, + MountPath: "/opt", + }, + } + + if rf.Spec.Redis.EnableTLS { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: RedisTLSVolumeName, + MountPath: "/tls", + }) + } + if rf.Spec.Auth.SecretPath != "" || secret != "" { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: redisAuthName, + MountPath: "/account", + }) + } + return volumeMounts +} + +func getRedisDataVolumeName(rf *v1.RedisFailover) string { + switch { + case rf.Spec.Redis.Storage.PersistentVolumeClaim != nil: + if rf.Spec.Redis.Storage.PersistentVolumeClaim.ObjectMeta.Name == "" { + return redisStorageVolumeName + } + return rf.Spec.Redis.Storage.PersistentVolumeClaim.ObjectMeta.Name + case rf.Spec.Redis.Storage.EmptyDir != nil: + return redisStorageVolumeName + default: + return redisStorageVolumeName + } +} + +func getRedisVolumes(inst types.RedisFailoverInstance, rf *v1.RedisFailover, secretName string) []corev1.Volume { + executeMode := int32(0400) + configname := GetRedisConfigMapName(rf) + volumes := []corev1.Volume{ + { + Name: redisConfigurationVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configname, + }, + DefaultMode: &executeMode, + }, + }, + }, + { + Name: RedisTmpVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + SizeLimit: resource.NewQuantity(1<<20, resource.BinarySI), //1Mi + }, + }, + }, + { + Name: redisOptName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + } + + if dataVolume := getRedisDataVolume(rf); dataVolume != nil { + volumes = append(volumes, *dataVolume) + } + if rf.Spec.Redis.EnableTLS { + volumes = append(volumes, corev1.Volume{ + Name: RedisTLSVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: builder.GetRedisSSLSecretName(rf.Name), + }, + }, + }) + } + if secretName != "" { + volumes = append(volumes, corev1.Volume{ + Name: redisAuthName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + }) + } + + if inst.IsStandalone() && NeedStandaloneInit(rf) { + if rf.Annotations[AnnotationStandaloneInitStorage] == "hostpath" { + // hostpath + if rf.Annotations[AnnotationStandaloneInitHostPath] != "" { + hostpathType := corev1.HostPathDirectory + volumes = append(volumes, corev1.Volume{ + Name: redisStandaloneVolumeName, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: rf.Annotations[AnnotationStandaloneInitHostPath], + Type: &hostpathType, + }, + }, + }) + } + } else { + // pvc + if rf.Annotations[AnnotationStandaloneInitPvcName] != "" { + volumes = append(volumes, corev1.Volume{ + Name: redisStandaloneVolumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: rf.Annotations[AnnotationStandaloneInitPvcName], + }, + }, + }) + } + } + } + return volumes +} + +func getRedisDataVolume(rf *v1.RedisFailover) *corev1.Volume { + // This will find the volumed desired by the user. If no volume defined + // an EmptyDir will be used by default + switch { + case rf.Spec.Redis.Storage.PersistentVolumeClaim != nil: + return nil + case rf.Spec.Redis.Storage.EmptyDir != nil: + return &corev1.Volume{ + Name: redisStorageVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: rf.Spec.Redis.Storage.EmptyDir, + }, + } + default: + return &corev1.Volume{ + Name: redisStorageVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + } +} diff --git a/internal/builder/failoverbuilder/status.go b/internal/builder/failoverbuilder/status.go new file mode 100644 index 0000000..c8da5f0 --- /dev/null +++ b/internal/builder/failoverbuilder/status.go @@ -0,0 +1,86 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failoverbuilder + +import ( + "encoding/json" + "fmt" + "math" + "strconv" + "time" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func ShouldUpdateDetailedStatusConfigMap(cm *corev1.ConfigMap, status *v1.RedisFailoverDetailedStatus) bool { + if cm.Data == nil || cm.Data["status"] == "" { + return true + } + + oldStatus := &v1.RedisFailoverDetailedStatus{} + if err := json.Unmarshal([]byte(cm.Data["status"]), oldStatus); err != nil { + return true + } + if oldStatus.Phase != status.Phase || oldStatus.Message != status.Message || len(oldStatus.Nodes) != len(status.Nodes) { + return true + } + + // if the last update is more than 5 minutes ago, we should update the status + tsVal := cm.Annotations["updateTimestamp"] + timestamp, _ := strconv.ParseInt(tsVal, 10, 64) + if timestamp+60*5 < time.Now().Unix() { + return true + } + + for i := 0; i < len(oldStatus.Nodes); i++ { + onode, nnode := oldStatus.Nodes[i], status.Nodes[i] + if onode.Role != nnode.Role || + onode.IP != nnode.IP || + onode.Port != nnode.Port || + onode.Version != nnode.Version || + onode.NodeName != nnode.NodeName || + + // if the memory usage is different than 1MB, we should update the status + math.Abs(float64(onode.UsedMemory-nnode.UsedMemory)) >= 1024*1024 || + math.Abs(float64(onode.UsedMemoryDataset-nnode.UsedMemoryDataset)) > 1024*1024 { + return true + } + } + return false +} + +// NewRedisFailoverDetailedStatusConfigMap creates a new ConfigMap for the given Cluster +func NewRedisFailoverDetailedStatusConfigMap(inst types.RedisFailoverInstance, status *v1.RedisFailoverDetailedStatus) (*corev1.ConfigMap, error) { + data, _ := json.MarshalIndent(status, "", " ") + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("rfr-%s-status", inst.GetName()), + Namespace: inst.GetNamespace(), + Labels: GetCommonLabels(inst.GetName(), nil), + Annotations: map[string]string{ + "updateTimestamp": fmt.Sprintf("%d", metav1.Now().Unix()), + }, + OwnerReferences: util.BuildOwnerReferencesWithParents(inst.Definition()), + }, + Data: map[string]string{"status": string(data)}, + }, nil +} diff --git a/api/redis/v1/sort.go b/internal/builder/helper.go similarity index 58% rename from api/redis/v1/sort.go rename to internal/builder/helper.go index f64953c..d1d8626 100644 --- a/api/redis/v1/sort.go +++ b/internal/builder/helper.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,19 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1 +package builder -// from new to old -type BackupSorterByCreateTime []RedisBackup +import "fmt" -func (b BackupSorterByCreateTime) Len() int { - return len(b) +// GenerateCertName +func GenerateCertName(name string) string { + return name + "-cert" } -func (b BackupSorterByCreateTime) Swap(i, j int) { - b[i], b[j] = b[j], b[i] +func GetRedisSSLSecretName(name string) string { + return fmt.Sprintf("%s-tls", name) } -func (b BackupSorterByCreateTime) Less(i, j int) bool { - return b[i].CreationTimestamp.After(b[j].CreationTimestamp.Time) +// GetServiceDNSName +func GetServiceDNSName(serviceName, namespace string) string { + return fmt.Sprintf("%s.%s.svc", serviceName, namespace) } diff --git a/internal/builder/sentinelbuilder/certificate.go b/internal/builder/sentinelbuilder/certificate.go new file mode 100644 index 0000000..7a1000c --- /dev/null +++ b/internal/builder/sentinelbuilder/certificate.go @@ -0,0 +1,49 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinelbuilder + +import ( + "time" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" + certv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + v12 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewCertificate +func NewCertificate(sen *v1.RedisSentinel, selectors map[string]string) *certv1.Certificate { + return &certv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: builder.GenerateCertName(sen.Name), + Namespace: sen.Namespace, + Labels: GetCommonLabels(sen.Name, selectors), + OwnerReferences: util.BuildOwnerReferences(sen), + }, + Spec: certv1.CertificateSpec{ + // 10 year + Duration: &metav1.Duration{Duration: 87600 * time.Hour}, + DNSNames: []string{ + builder.GetServiceDNSName(GetSentinelStatefulSetName(sen.Name), sen.Namespace), + }, + IssuerRef: v12.ObjectReference{Kind: certv1.ClusterIssuerKind, Name: "cpaas-ca"}, + SecretName: builder.GetRedisSSLSecretName(sen.Name), + }, + } +} diff --git a/internal/builder/sentinelbuilder/configmap.go b/internal/builder/sentinelbuilder/configmap.go new file mode 100644 index 0000000..53ae4ce --- /dev/null +++ b/internal/builder/sentinelbuilder/configmap.go @@ -0,0 +1,93 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinelbuilder + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/alauda/redis-operator/api/core" + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + constConfig = map[string]string{ + "dir": "/data", + } +) + +func NewSentinelConfigMap(sen *v1.RedisSentinel, selectors map[string]string) (*corev1.ConfigMap, error) { + defaultConfig := make(map[string]string) + defaultConfig["loglevel"] = "notice" + defaultConfig["maxclients"] = "10000" + defaultConfig["tcp-keepalive"] = "300" + defaultConfig["tcp-backlog"] = "511" + + version, _ := redis.ParseRedisVersionFromImage(sen.Spec.Image) + innerRedisConfig := version.CustomConfigs(core.RedisStdSentinel) + defaultConfig = lo.Assign(defaultConfig, innerRedisConfig) + + for k, v := range sen.Spec.CustomConfig { + defaultConfig[strings.ToLower(k)] = strings.TrimSpace(v) + } + for k, v := range constConfig { + defaultConfig[strings.ToLower(k)] = strings.TrimSpace(v) + } + + keys := make([]string, 0, len(defaultConfig)) + for k := range defaultConfig { + keys = append(keys, k) + } + sort.Strings(keys) + + var buffer bytes.Buffer + for _, k := range keys { + v := defaultConfig[k] + if v == "" || v == `""` { + buffer.WriteString(fmt.Sprintf("%s \"\"\n", k)) + continue + } + + if _, ok := builder.MustQuoteRedisConfig[k]; ok && !strings.HasPrefix(v, `"`) { + v = fmt.Sprintf(`"%s"`, v) + } + if _, ok := builder.MustUpperRedisConfig[k]; ok { + v = strings.ToUpper(v) + } + buffer.WriteString(fmt.Sprintf("%s %s\n", k, v)) + } + + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetSentinelConfigMapName(sen.GetName()), + Namespace: sen.Namespace, + Labels: GetCommonLabels(sen.Name, selectors), + OwnerReferences: util.BuildOwnerReferences(sen), + }, + Data: map[string]string{ + SentinelConfigFileName: buffer.String(), + }, + }, nil +} diff --git a/internal/builder/sentinelbuilder/helper.go b/internal/builder/sentinelbuilder/helper.go new file mode 100644 index 0000000..45772aa --- /dev/null +++ b/internal/builder/sentinelbuilder/helper.go @@ -0,0 +1,74 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinelbuilder + +import ( + "fmt" + + "github.com/alauda/redis-operator/internal/builder" + "github.com/samber/lo" +) + +const ( + SentinelConfigFileName = "sentinel.conf" +) + +func GetCommonLabels(name string, extra ...map[string]string) map[string]string { + return lo.Assign(lo.Assign(extra...), getPublicLabels(name)) +} + +func getPublicLabels(name string) map[string]string { + return map[string]string{ + "app.kubernetes.io/component": RedisArchRoleSEN, + "app.kubernetes.io/managed-by": "redis-operator", + builder.InstanceNameLabel: name, + builder.InstanceTypeLabel: "redis-failover", + "redissentinels.databases.spotahome.com/name": name, + } +} + +func GenerateSelectorLabels(component, name string) map[string]string { + // NOTE: not use "middleware.instance/name" and "middleware.instance/type" for compatibility for old instances + return map[string]string{ + "app.kubernetes.io/name": name, + "redissentinels.databases.spotahome.com/name": name, + "app.kubernetes.io/component": component, + } +} + +func GetSentinelConfigMapName(name string) string { + // NOTE: use rfs-server-xxx for compatibility for old name "rfs-xxx" maybe needed + return fmt.Sprintf("rfs-server-%s", name) +} + +func GetSentinelStatefulSetName(sentinelName string) string { + return fmt.Sprintf("rfs-%s", sentinelName) +} + +func GetSentinelServiceName(sentinelName string) string { + return fmt.Sprintf("rfs-%s", sentinelName) +} + +func GetSentinelHeadlessServiceName(sentinelName string) string { + // NOTE: use rfs-server-xxx-hl for compatibility for old name "rfs-xxx-hl" + // this naming method may cause name conflict with -hl + return fmt.Sprintf("rfs-%s-hl", sentinelName) +} + +func GetSentinelNodeServiceName(sentinelName string, i int) string { + return fmt.Sprintf("rfs-%s-%d", sentinelName, i) +} diff --git a/internal/builder/sentinelbuilder/poddisruptionbudget.go b/internal/builder/sentinelbuilder/poddisruptionbudget.go new file mode 100644 index 0000000..2343a15 --- /dev/null +++ b/internal/builder/sentinelbuilder/poddisruptionbudget.go @@ -0,0 +1,49 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinelbuilder + +import ( + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/util" + "github.com/samber/lo" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func NewPodDisruptionBudget(sen *databasesv1.RedisSentinel, selectors map[string]string) *policyv1.PodDisruptionBudget { + maxUnavailable := intstr.FromInt(int(sen.Spec.Replicas) / 2) + namespace := sen.GetNamespace() + selectors = lo.Assign(selectors, GenerateSelectorLabels(RedisArchRoleSEN, sen.Name)) + labels := lo.Assign(GetCommonLabels(sen.Name), GenerateSelectorLabels(RedisArchRoleSEN, sen.Name), selectors) + + name := GetSentinelStatefulSetName(sen.Name) + return &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Name: name, + Namespace: namespace, + OwnerReferences: util.BuildOwnerReferences(sen), + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: &maxUnavailable, + Selector: &metav1.LabelSelector{ + MatchLabels: selectors, + }, + }, + } +} diff --git a/internal/builder/sentinelbuilder/service.go b/internal/builder/sentinelbuilder/service.go new file mode 100644 index 0000000..3ea7bf4 --- /dev/null +++ b/internal/builder/sentinelbuilder/service.go @@ -0,0 +1,211 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinelbuilder + +import ( + "fmt" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/util" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + RedisArchRoleSEN = "sentinel" + RedisSentinelSVCPort = 26379 +) + +func NewSentinelServiceForCR(inst *v1.RedisSentinel, selectors map[string]string) *corev1.Service { + var ( + namespace = inst.Namespace + name = GetSentinelServiceName(inst.Name) + ptype = corev1.IPFamilyPolicySingleStack + protocol = []corev1.IPFamily{} + ) + if inst.Spec.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + protocol = append(protocol, corev1.IPv6Protocol) + } else { + protocol = append(protocol, corev1.IPv4Protocol) + } + + selectorLabels := lo.Assign(GenerateSelectorLabels(RedisArchRoleSEN, inst.Name), GetCommonLabels(inst.Name)) + if len(selectors) > 0 { + selectorLabels = lo.Assign(selectors, GenerateSelectorLabels(RedisArchRoleSEN, inst.Name)) + } + // NOTE: remove this label for compatibility for old instances + // TODO: remove this in 3.22 + delete(selectorLabels, "redissentinels.databases.spotahome.com/name") + labels := GetCommonLabels(inst.Name, GenerateSelectorLabels(RedisArchRoleSEN, inst.Name), selectorLabels) + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: inst.Spec.Expose.Annotations, + OwnerReferences: util.BuildOwnerReferences(inst), + }, + Spec: corev1.ServiceSpec{ + Type: inst.Spec.Expose.ServiceType, + IPFamilyPolicy: &ptype, + IPFamilies: protocol, + Selector: selectorLabels, + Ports: []corev1.ServicePort{ + { + Name: SentinelContainerPortName, + Port: RedisSentinelSVCPort, + TargetPort: intstr.FromInt(RedisSentinelSVCPort), + Protocol: "TCP", + NodePort: inst.Spec.Expose.AccessPort, + }, + }, + }, + } +} + +func NewSentinelHeadlessServiceForCR(inst *v1.RedisSentinel, selectors map[string]string) *corev1.Service { + name := GetSentinelHeadlessServiceName(inst.Name) + namespace := inst.Namespace + selectorLabels := GenerateSelectorLabels(RedisArchRoleSEN, inst.Name) + labels := GetCommonLabels(inst.Name, selectors, selectorLabels) + labels[builder.LabelRedisArch] = RedisArchRoleSEN + annotations := inst.Spec.Expose.Annotations + ptype := corev1.IPFamilyPolicySingleStack + protocol := []corev1.IPFamily{} + if inst.Spec.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + protocol = append(protocol, corev1.IPv6Protocol) + } else { + protocol = append(protocol, corev1.IPv4Protocol) + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, + OwnerReferences: util.BuildOwnerReferences(inst), + }, + Spec: corev1.ServiceSpec{ + IPFamilies: protocol, + IPFamilyPolicy: &ptype, + Type: corev1.ServiceTypeClusterIP, + ClusterIP: corev1.ClusterIPNone, + Selector: selectorLabels, + Ports: []corev1.ServicePort{ + { + Name: SentinelContainerPortName, + Port: RedisSentinelSVCPort, + TargetPort: intstr.FromInt(RedisSentinelSVCPort), + Protocol: "TCP", + }, + }, + }, + } +} + +func NewRedisNodePortService(inst *v1.RedisSentinel, index int, nodePort int32, selectors map[string]string) *corev1.Service { + var ( + namespace = inst.Namespace + name = fmt.Sprintf("%s-%d", GetSentinelStatefulSetName(inst.Name), index) + ptype = corev1.IPFamilyPolicySingleStack + protocol = []corev1.IPFamily{} + ) + if inst.Spec.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + protocol = append(protocol, corev1.IPv6Protocol) + } else { + protocol = append(protocol, corev1.IPv4Protocol) + } + labels := lo.Assign(GetCommonLabels(inst.Name), selectors, GenerateSelectorLabels(RedisArchRoleSEN, inst.Name)) + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: inst.Spec.Expose.Annotations, + OwnerReferences: util.BuildOwnerReferences(inst), + }, + Spec: corev1.ServiceSpec{ + IPFamilies: protocol, + IPFamilyPolicy: &ptype, + Type: corev1.ServiceTypeNodePort, + Ports: []corev1.ServicePort{ + { + Port: RedisSentinelSVCPort, + TargetPort: intstr.FromInt(RedisSentinelSVCPort), + Protocol: corev1.ProtocolTCP, + Name: SentinelContainerPortName, + NodePort: nodePort, + }, + }, + Selector: map[string]string{builder.PodNameLabelKey: name}, + }, + } +} + +// NewPodService returns a new Service for the given RedisFailover and index, with the configed service type +func NewPodService(sen *v1.RedisSentinel, index int, selectors map[string]string) *corev1.Service { + return NewPodNodePortService(sen, index, selectors, 0) +} + +func NewPodNodePortService(sen *v1.RedisSentinel, index int, selectors map[string]string, nodePort int32) *corev1.Service { + var ( + namespace = sen.Namespace + name = GetSentinelNodeServiceName(sen.Name, index) + ptype = corev1.IPFamilyPolicySingleStack + protocol = []corev1.IPFamily{} + ) + if sen.Spec.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + protocol = append(protocol, corev1.IPv6Protocol) + } else { + protocol = append(protocol, corev1.IPv4Protocol) + } + labels := lo.Assign(GetCommonLabels(sen.Name), selectors, GenerateSelectorLabels(RedisArchRoleSEN, sen.Name)) + selectorLabels := map[string]string{ + builder.PodNameLabelKey: name, + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: sen.Spec.Expose.Annotations, + OwnerReferences: util.BuildOwnerReferences(sen), + }, + Spec: corev1.ServiceSpec{ + IPFamilies: protocol, + IPFamilyPolicy: &ptype, + Type: corev1.ServiceTypeNodePort, + Ports: []corev1.ServicePort{ + { + Port: RedisSentinelSVCPort, + TargetPort: intstr.FromInt(RedisSentinelSVCPort), + Protocol: corev1.ProtocolTCP, + Name: SentinelContainerPortName, + NodePort: nodePort, + }, + }, + Selector: selectorLabels, + }, + } +} diff --git a/internal/builder/sentinelbuilder/statefulset.go b/internal/builder/sentinelbuilder/statefulset.go new file mode 100644 index 0000000..a935260 --- /dev/null +++ b/internal/builder/sentinelbuilder/statefulset.go @@ -0,0 +1,468 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinelbuilder + +import ( + "fmt" + "path" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types" + "github.com/samber/lo" + appv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" +) + +const ( + SentinelConfigVolumeName = "sentinel-config" + SentinelConfigVolumeMountPath = "/conf" + RedisTLSVolumeName = "redis-tls" + RedisTLSVolumeMountPath = "/tls" + RedisDataVolumeName = "data" + RedisDataVolumeMountPath = "/data" + RedisAuthName = "redis-auth" + RedisAuthMountPath = "/account" + RedisOptName = "redis-opt" + RedisOptMountPath = "/opt" + OperatorUsername = "OPERATOR_USERNAME" + OperatorSecretName = "OPERATOR_SECRET_NAME" + SentinelContainerName = "sentinel" + SentinelContainerPortName = "sentinel" + graceTime = 30 +) + +func NewSentinelStatefulset(sen types.RedisSentinelInstance, selectors map[string]string) *appv1.StatefulSet { + var ( + inst = sen.Definition() + passwordSecret = inst.Spec.PasswordSecret + ) + if len(selectors) == 0 { + selectors = GenerateSelectorLabels(RedisArchRoleSEN, inst.GetName()) + } else { + selectors = lo.Assign(selectors, GenerateSelectorLabels(RedisArchRoleSEN, inst.GetName())) + } + labels := lo.Assign(GetCommonLabels(inst.GetName()), selectors) + + startArgs := []string{"sh", "/opt/run_sentinel.sh"} + shutdownArgs := []string{"sh", "-c", "/opt/redis-tools sentinel shutdown &> /proc/1/fd/1"} + volumes := getVolumes(inst, passwordSecret) + volumeMounts := getRedisVolumeMounts(inst, passwordSecret) + envs := createRedisContainerEnvs(inst) + + localhost := "127.0.0.1" + if inst.Spec.Expose.IPFamilyPrefer == corev1.IPv6Protocol { + localhost = "::1" + } + ss := &appv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetSentinelStatefulSetName(inst.GetName()), + Namespace: inst.GetNamespace(), + Labels: labels, + OwnerReferences: util.BuildOwnerReferences(inst), + }, + Spec: appv1.StatefulSetSpec{ + ServiceName: GetSentinelHeadlessServiceName(inst.GetName()), + Replicas: &inst.Spec.Replicas, + PodManagementPolicy: appv1.ParallelPodManagement, + UpdateStrategy: appv1.StatefulSetUpdateStrategy{ + Type: appv1.RollingUpdateStatefulSetStrategyType, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: selectors, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: inst.Spec.PodAnnotations, + }, + Spec: corev1.PodSpec{ + HostAliases: []corev1.HostAlias{ + {IP: localhost, Hostnames: []string{config.LocalInjectName}}, + }, + Containers: []corev1.Container{ + { + Name: SentinelContainerName, + Command: startArgs, + Image: inst.Spec.Image, + ImagePullPolicy: builder.GetPullPolicy(inst.Spec.ImagePullPolicy), + Env: envs, + Ports: []corev1.ContainerPort{ + { + Name: SentinelContainerPortName, + ContainerPort: 26379, + Protocol: corev1.ProtocolTCP, + }, + }, + StartupProbe: &corev1.Probe{ + InitialDelaySeconds: 3, + TimeoutSeconds: 5, + FailureThreshold: 3, + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(26379), + }, + }, + }, + LivenessProbe: &corev1.Probe{ + InitialDelaySeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 5, + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(26379), + }, + }, + }, + Resources: inst.Spec.Resources, + Lifecycle: &corev1.Lifecycle{ + PreStop: &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: shutdownArgs, + }, + }, + }, + SecurityContext: &corev1.SecurityContext{ + ReadOnlyRootFilesystem: pointer.Bool(true), + }, + VolumeMounts: volumeMounts, + }, + }, + ImagePullSecrets: inst.Spec.ImagePullSecrets, + SecurityContext: builder.GetPodSecurityContext(inst.Spec.SecurityContext), + ServiceAccountName: clusterbuilder.RedisInstanceServiceAccountName, + Affinity: getAffinity(inst.Spec.Affinity, selectors), + Tolerations: inst.Spec.Tolerations, + NodeSelector: inst.Spec.NodeSelector, + Volumes: volumes, + TerminationGracePeriodSeconds: pointer.Int64(graceTime), + }, + }, + }, + } + + ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, *buildInitContainer(inst, nil)) + ss.Spec.Template.Spec.Containers = append(ss.Spec.Template.Spec.Containers, *buildAgentContainer(inst, envs)) + + if inst.Spec.Expose.ServiceType == corev1.ServiceTypeNodePort || + inst.Spec.Expose.ServiceType == corev1.ServiceTypeLoadBalancer { + + ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, buildExposeContainer(inst)) + } + return ss +} + +func buildExposeContainer(inst *v1.RedisSentinel) corev1.Container { + container := corev1.Container{ + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + }, + Name: "expose", + Image: config.GetRedisToolsImage(inst), + ImagePullPolicy: builder.GetPullPolicy(inst.Spec.ImagePullPolicy), + VolumeMounts: []corev1.VolumeMount{ + {Name: RedisDataVolumeName, MountPath: RedisDataVolumeMountPath}, + }, + Env: []corev1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: "NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, { + Name: "SENTINEL_ANNOUNCE_PATH", + Value: "/data/announce.conf", + }, + { + Name: "IP_FAMILY_PREFER", + Value: string(inst.Spec.Expose.IPFamilyPrefer), + }, + { + Name: "SERVICE_TYPE", + Value: string(inst.Spec.Expose.ServiceType), + }, + }, + Command: []string{"/opt/redis-tools", "sentinel", "expose"}, + SecurityContext: builder.GetSecurityContext(inst.Spec.SecurityContext), + } + return container +} + +func buildInitContainer(inst *v1.RedisSentinel, _ []corev1.EnvVar) *corev1.Container { + image := config.GetRedisToolsImage(inst) + if image == "" { + return nil + } + + return &corev1.Container{ + Name: "init", + Image: image, + ImagePullPolicy: util.GetPullPolicy(inst.Spec.ImagePullPolicy), + Command: []string{"sh", "/opt/init_sentinel.sh"}, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + {Name: RedisOptName, MountPath: path.Join("/mnt", RedisOptMountPath)}, + }, + } +} + +func buildAgentContainer(inst *v1.RedisSentinel, envs []corev1.EnvVar) *corev1.Container { + image := config.GetRedisToolsImage(inst) + if image == "" { + return nil + } + container := corev1.Container{ + Name: "agent", + Image: image, + ImagePullPolicy: util.GetPullPolicy(inst.Spec.ImagePullPolicy), + Env: envs, + Command: []string{"/opt/redis-tools", "sentinel", "agent"}, + SecurityContext: builder.GetSecurityContext(inst.Spec.SecurityContext), + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: RedisDataVolumeName, + MountPath: RedisDataVolumeMountPath, + }, + }, + } + + if inst.Spec.PasswordSecret != "" { + vol := corev1.VolumeMount{ + Name: RedisAuthName, + MountPath: RedisAuthMountPath, + } + container.VolumeMounts = append(container.VolumeMounts, vol) + } + if inst.Spec.EnableTLS { + vol := corev1.VolumeMount{ + Name: RedisTLSVolumeName, + MountPath: RedisTLSVolumeMountPath, + } + container.VolumeMounts = append(container.VolumeMounts, vol) + } + return &container +} + +func createRedisContainerEnvs(inst *v1.RedisSentinel) []corev1.EnvVar { + redisEnvs := []corev1.EnvVar{ + { + Name: "NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + { + Name: "POD_UID", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.uid", + }, + }, + }, + { + Name: "POD_IP", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }, + { + Name: "POD_IPS", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIPs", + }, + }, + }, + { + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: OperatorSecretName, + Value: inst.Spec.PasswordSecret, + }, + { + Name: "TLS_ENABLED", + Value: fmt.Sprintf("%t", inst.Spec.EnableTLS), + }, + { + Name: "SERVICE_TYPE", + Value: string(inst.Spec.Expose.ServiceType), + }, + { + Name: "IP_FAMILY_PREFER", + Value: string(inst.Spec.Expose.IPFamilyPrefer), + }, + } + return redisEnvs +} + +func getAffinity(affinity *corev1.Affinity, labels map[string]string) *corev1.Affinity { + if affinity != nil { + return affinity + } + + // Return a SOFT anti-affinity + return &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ + { + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + TopologyKey: builder.HostnameTopologyKey, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + }, + }, + }, + }, + } +} + +func getRedisVolumeMounts(inst *v1.RedisSentinel, secretName string) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{ + { + Name: SentinelConfigVolumeName, + MountPath: SentinelConfigVolumeMountPath, + }, + { + Name: RedisDataVolumeName, + MountPath: RedisDataVolumeMountPath, + }, + { + Name: RedisOptName, + MountPath: RedisOptMountPath, + }, + } + + if inst.Spec.EnableTLS { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: RedisTLSVolumeName, + MountPath: RedisTLSVolumeMountPath, + }) + } + if secretName != "" { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: RedisAuthName, + MountPath: RedisAuthMountPath, + }) + } + return volumeMounts +} + +func getVolumes(inst *v1.RedisSentinel, secretName string) []corev1.Volume { + volumes := []corev1.Volume{ + { + Name: SentinelConfigVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: GetSentinelConfigMapName(inst.GetName()), + }, + DefaultMode: pointer.Int32(0400), + }, + }, + }, + { + Name: RedisDataVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: RedisOptName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + } + if inst.Spec.EnableTLS { + volumes = append(volumes, corev1.Volume{ + Name: RedisTLSVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: builder.GetRedisSSLSecretName(inst.Name), + DefaultMode: pointer.Int32(0400), + }, + }, + }) + } + if secretName != "" { + volumes = append(volumes, corev1.Volume{ + Name: RedisAuthName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + DefaultMode: pointer.Int32(0400), + }, + }, + }) + } + return volumes +} diff --git a/internal/builder/util.go b/internal/builder/util.go new file mode 100644 index 0000000..da0b9c5 --- /dev/null +++ b/internal/builder/util.go @@ -0,0 +1,99 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/pointer" +) + +func BuildMetricsRegex(regex []string) string { + uniqueArr := func(m []string) []string { + d := make([]string, 0) + result := make(map[string]bool, len(m)) + for _, v := range m { + if !result[v] { + result[v] = true + d = append(d, v) + } + } + return d + } + return fmt.Sprintf("(%s)", strings.Join(uniqueArr(regex), "|")) +} + +func GetPullPolicy(policies ...corev1.PullPolicy) corev1.PullPolicy { + for _, policy := range policies { + if policy != "" { + return policy + } + } + return corev1.PullAlways +} + +func GenerateRedisTLSOptions() string { + return "--tls --cert /tls/tls.crt --key /tls/tls.key --cacert /tls/ca.crt" +} + +func GetPodSecurityContext(secctx *corev1.PodSecurityContext) (podSec *corev1.PodSecurityContext) { + // 999 is the default userid for redis offical docker image + // 1000 is the default groupid for redis offical docker image + _, groupId := int64(999), int64(1000) + if secctx == nil { + podSec = &corev1.PodSecurityContext{FSGroup: &groupId} + } else { + podSec = &corev1.PodSecurityContext{} + if secctx.FSGroup != nil { + podSec.FSGroup = secctx.FSGroup + } + } + return +} + +func GetSecurityContext(secctx *corev1.PodSecurityContext) (sec *corev1.SecurityContext) { + // 999 is the default userid for redis offical docker image + // 1000 is the default groupid for redis offical docker image + userId, groupId := int64(999), int64(1000) + if secctx == nil { + sec = &corev1.SecurityContext{ + RunAsUser: &userId, + RunAsGroup: &groupId, + RunAsNonRoot: pointer.Bool(true), + ReadOnlyRootFilesystem: pointer.Bool(true), + } + } else { + sec = &corev1.SecurityContext{ + RunAsUser: &userId, + RunAsGroup: &groupId, + RunAsNonRoot: pointer.Bool(true), + ReadOnlyRootFilesystem: pointer.Bool(true), + } + if secctx.RunAsUser != nil { + sec.RunAsUser = secctx.RunAsUser + } + if secctx.RunAsGroup != nil { + sec.RunAsGroup = secctx.RunAsGroup + } + if secctx.RunAsNonRoot != nil { + sec.RunAsNonRoot = secctx.RunAsNonRoot + } + } + return +} diff --git a/pkg/config/defaults.go b/internal/config/defaults.go similarity index 56% rename from pkg/config/defaults.go rename to internal/config/defaults.go index e582114..d21c31c 100644 --- a/pkg/config/defaults.go +++ b/internal/config/defaults.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -22,6 +22,7 @@ import ( "strings" "github.com/Masterminds/semver/v3" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ( @@ -29,17 +30,20 @@ var ( ErrInvalidImage = errors.New("invalid source image") ) -// var ( -// minVerionOfRedisTLSSupported, _ = semver.NewVersion("6.0-AAA") -// ) +const DefaultRedisVersion = "6.0" var redisVersionEnvs = []string{ "REDIS_VERSION_4_IMAGE", "REDIS_VERSION_5_IMAGE", "REDIS_VERSION_6_IMAGE", + "REDIS_VERSION_7_IMAGE", "REDIS_VERSION_7_2_IMAGE", } +func GetOperatorVersion() string { + return os.Getenv("REDIS_OPERATOR_VERSION") +} + func GetRedisVersion(image string) string { if image == "" { image = GetDefaultRedisImage() @@ -90,30 +94,48 @@ func GetDefaultRedisImage() string { return Getenv("DEFAULT_REDIS_IMAGE") } -func GetRedisImage(src string) (string, error) { - if src == "" { - if val := GetDefaultRedisImage(); val == "" { - return "", ErrImageNotFound - } else { +func GetRedisImageByVersion(version string) (string, error) { + wantedVersion, err := semver.NewVersion(version) + if err != nil { + return "", ErrInvalidImage + } + + var lastErr error + for _, name := range redisVersionEnvs { + val := os.Getenv(name) + if val == "" { + continue + } + fields := strings.SplitN(val, ":", 2) + if len(fields) != 2 { + continue + } + + ver, err := semver.NewVersion(fields[1]) + if err != nil { + lastErr = err + continue + } + if ver.Major() == wantedVersion.Major() && ver.Minor() == wantedVersion.Minor() { return val, nil } } - - fields := strings.SplitN(src, ":", 2) - if len(fields) != 2 { - return "", ErrInvalidImage + if lastErr != nil { + return "", lastErr } - return GetRedisImageByVersion(fields[1]) + return "", ErrImageNotFound } -func GetRedisImageByVersion(version string) (string, error) { +func GetActiveRedisImageByVersion(version string) (string, error) { wantedVersion, err := semver.NewVersion(version) if err != nil { return "", ErrInvalidImage } var lastErr error - for _, name := range redisVersionEnvs { + for _, name := range []string{ + "ACTIVE_REDIS_VERSION_6_IMAGE", + } { val := os.Getenv(name) if val == "" { continue @@ -138,14 +160,56 @@ func GetRedisImageByVersion(version string) (string, error) { return "", ErrImageNotFound } -func GetDefaultBackupImage() string { - return Getenv("REDIS_TOOLS_IMAGE") +func BuildImageVersionKey(typ string) string { + return ImageVersionKeyPrefix + typ +} + +func GetProxyImage(obj v1.Object) string { + key := ImageVersionKeyPrefix + "redis-proxy" + if obj != nil { + if val := obj.GetAnnotations()[key]; val != "" { + return val + } + } + return Getenv("DEFAULT_PROXY_IMAGE") } -func GetRedisToolsImage() string { +func GetShakeImage(obj v1.Object) string { + key := ImageVersionKeyPrefix + "redis-shake" + if obj != nil { + if val := obj.GetAnnotations()[key]; val != "" { + return val + } + } + return Getenv("DEFAULT_SHAKE_IMAGE") +} + +func GetRedisToolsImage(obj v1.Object) string { + key := ImageVersionKeyPrefix + "redis-tools" + if obj != nil { + if val := obj.GetAnnotations()[key]; val != "" { + return val + } + } return Getenv("REDIS_TOOLS_IMAGE") } -func GetDefaultExporterImage() string { - return Getenv("REDIS_EXPORTER_IMAGE", Getenv("DEFAULT_EXPORTER_IMAGE", "build-harbor.alauda.cn/middleware/oliver006/redis_exporter:v1.3.5-alpine")) +func GetRedisExporterImage(obj v1.Object) string { + key := ImageVersionKeyPrefix + "redis-exporter" + if obj != nil { + if val := obj.GetAnnotations()[key]; val != "" { + return val + } + } + return Getenv("REDIS_EXPORTER_IMAGE", Getenv("DEFAULT_EXPORTER_IMAGE")) +} + +func GetRedisExposeImage(obj v1.Object) string { + key := ImageVersionKeyPrefix + "expose-pod" + if obj != nil { + if val := obj.GetAnnotations()[key]; val != "" { + return val + } + } + return "" } diff --git a/pkg/config/defaults_test.go b/internal/config/defaults_test.go similarity index 96% rename from pkg/config/defaults_test.go rename to internal/config/defaults_test.go index a021838..7e21503 100644 --- a/pkg/config/defaults_test.go +++ b/internal/config/defaults_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/config/event.go b/internal/config/event.go new file mode 100644 index 0000000..b21ae08 --- /dev/null +++ b/internal/config/event.go @@ -0,0 +1,33 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +type EventReason = string + +const ( + EventPause EventReason = "Paused" + EventCreateUser EventReason = "CreatedUser" + EventUpdateUser EventReason = "UpdatedUser" + EventUpdatePassword EventReason = "UpdatedPassword" + EventCleanResource EventReason = "CleanResource" + EventFailover EventReason = "Failover" + + EventSetupMaster EventReason = "SetupMaster" + EventAllocateSlots EventReason = "AllocatedSlots" + EventAssignSlots EventReason = "AssignSlots" + EventRebalance EventReason = "Rebalancing" +) diff --git a/internal/config/keys.go b/internal/config/keys.go new file mode 100644 index 0000000..c5bcc60 --- /dev/null +++ b/internal/config/keys.go @@ -0,0 +1,49 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +const ( + RedisSecretUsernameKey = "username" + RedisSecretPasswordKey = "password" // #nosec + + S3_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID" // #nosec + S3_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY" // #nosec + S3_TOKEN = "TOKEN" // #nosec + S3_REGION = "REGION" + S3_ENDPOINTURL = "ENDPOINTURL" + + PAUSE_ANNOTATION_KEY = "app.cpaas.io/pause-timestamp" + + // DNS + LocalInjectName = "local.inject" + + ImageVersionKeyPrefix = "middleware.instance/imageversions-" +) + +// Version Controller related keys +const ( + InstanceTypeKey = "middleware.instance/type" + CRUpgradeableVersion = "middleware.upgrade.crVersion" + CRUpgradeableComponentVersion = "middleware.upgrade.component.version" + CRAutoUpgradeKey = "middleware.instance/autoUpgrade" + LatestKey = "middleware.instance/latest" + CRVersionKey = "middleware.instance/crVersion" + CRVersionSHAKey = "middleware.instance/crVersion-sha" + CoreComponentName = "redis" + + OperatorVersionAnnotation = "operatorVersion" +) diff --git a/internal/controller/redis.kun/distributedrediscluster_controller.go b/internal/controller/cluster/distributedrediscluster_controller.go similarity index 63% rename from internal/controller/redis.kun/distributedrediscluster_controller.go rename to internal/controller/cluster/distributedrediscluster_controller.go index bbded61..7a9f818 100644 --- a/internal/controller/redis.kun/distributedrediscluster_controller.go +++ b/internal/controller/cluster/distributedrediscluster_controller.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,6 +18,7 @@ package cluster import ( "context" + "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -28,10 +29,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" - clusterv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/ops" + clusterv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/ops" ) // DistributedRedisClusterReconciler reconciles a DistributedRedisCluster object @@ -42,11 +45,6 @@ type DistributedRedisClusterReconciler struct { Engine *ops.OpEngine } -const ( - ControllerVersion = "v1alpha1" - ControllerVersionLabel = "controllerVersion" -) - //+kubebuilder:rbac:groups=redis.kun,resources=distributedredisclusters,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=redis.kun,resources=distributedredisclusters/status,verbs=get;update;patch //+kubebuilder:rbac:groups=redis.kun,resources=distributedredisclusters/finalizers,verbs=update @@ -64,14 +62,6 @@ func (r *DistributedRedisClusterReconciler) Reconcile(ctx context.Context, req c return ctrl.Result{}, err } - // switch for turn /on off this operator - if instance.Labels[ControllerVersionLabel] == ControllerVersion { - return ctrl.Result{}, nil - } - - // NOTE: make sure not overwrite the labels from cr - delete(instance.Labels, ControllerVersionLabel) - // update default status if instance.Status.Status == "" && len(instance.Status.Shards) == 0 { // update status to creating @@ -87,11 +77,68 @@ func (r *DistributedRedisClusterReconciler) Reconcile(ctx context.Context, req c } } + if instance.Spec.EnableActiveRedis && (instance.Spec.ServiceID == nil || *instance.Spec.ServiceID < 0 || *instance.Spec.ServiceID > 15) { + instance.Status.Status = clusterv1alpha1.ClusterStatusKO + instance.Status.Reason = "service id must be in [0, 15]" + _ = r.Status().Update(ctx, &instance) + return ctrl.Result{}, nil + } + + crVersion := instance.Annotations[config.CRVersionKey] + if crVersion == "" { + managedByRds := func() bool { + for _, ref := range instance.GetOwnerReferences() { + if ref.Kind == "Redis" { + return true + } + } + return false + }() + if managedByRds { + logger.Info("no actor version specified, waiting for rds register rds version") + return ctrl.Result{RequeueAfter: time.Second * 15}, nil + } else if config.GetOperatorVersion() != "" { + // update crVersion to instance + instance.Annotations[config.CRVersionKey] = config.GetOperatorVersion() + if err := r.Update(ctx, &instance); err != nil { + logger.Error(err, "update instance actor version failed") + } + return ctrl.Result{RequeueAfter: time.Second}, nil + } + } + // ================ setup default =================== - // TODO: to this in webhook - // here do some default value check and convert - _ = instance.Init() + _ = instance.Default() + + var user redismiddlewarealaudaiov1.RedisUser + if err := r.Get(ctx, client.ObjectKey{ + Namespace: instance.Namespace, + Name: clusterbuilder.GenerateClusterDefaultRedisUserName(instance.GetName()), + }, &user); err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + } else { + isUpdated := false + if instance.Spec.PasswordSecret != nil && instance.Spec.PasswordSecret.Name != "" { + secretName := instance.Spec.PasswordSecret.Name + if len(user.Spec.PasswordSecrets) == 0 || user.Spec.PasswordSecrets[0] != secretName { + user.Spec.PasswordSecrets = append(user.Spec.PasswordSecrets[0:0], secretName) + isUpdated = true + } + } else { + if len(user.Spec.PasswordSecrets) != 0 { + user.Spec.PasswordSecrets = nil + isUpdated = true + } + } + if isUpdated { + if err := r.Update(ctx, &user); err != nil { + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + } + } // ================ setup default end =================== @@ -102,10 +149,12 @@ func (r *DistributedRedisClusterReconciler) Reconcile(ctx context.Context, req c func (r *DistributedRedisClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&clusterv1alpha1.DistributedRedisCluster{}). - WithEventFilter(predicate.GenerationChangedPredicate{}). - WithOptions(controller.Options{MaxConcurrentReconciles: 3}). + // WithEventFilter(predicate.GenerationChangedPredicate{}). + WithOptions(controller.Options{MaxConcurrentReconciles: 16}). Owns(&appsv1.StatefulSet{}). Owns(&corev1.Service{}). + Owns(&corev1.ConfigMap{}). + Owns(&corev1.Secret{}). Complete(r) } diff --git a/internal/controller/cluster/distributedrediscluster_controller_test.go b/internal/controller/cluster/distributedrediscluster_controller_test.go new file mode 100644 index 0000000..7317b8a --- /dev/null +++ b/internal/controller/cluster/distributedrediscluster_controller_test.go @@ -0,0 +1,134 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cluster + +import ( + "context" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + rediskunv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/ops" + _ "github.com/alauda/redis-operator/internal/ops/cluster/actor" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes/clientset" +) + +var _ = Describe("DistributedRedisClusterReconciler", func() { + Context("When reconciling a resource", func() { + const ( + resourceName = "redis-cluster" + namespace = "default" + ) + + ctx := context.Background() + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + } + inst := &rediskunv1alpha1.DistributedRedisCluster{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Cron") + err := k8sClient.Get(ctx, typeNamespacedName, inst) + if err != nil && errors.IsNotFound(err) { + resource := &rediskunv1alpha1.DistributedRedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: rediskunv1alpha1.DistributedRedisClusterSpec{ + Image: "redis:6.0.20", + MasterSize: 3, + ClusterReplicas: 1, + Config: map[string]string{ + "save": "900 1", + }, + AffinityPolicy: core.AntiAffinityInSharding, + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("4Gi"), + }, + }, + IPFamilyPrefer: corev1.IPv4Protocol, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &rediskunv1alpha1.DistributedRedisCluster{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Cron") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + + var ( + eventRecorder = record.NewFakeRecorder(10) + logger, _ = logr.FromContext(context.TODO()) + + clientset = clientset.NewWithConfig(k8sClient, cfg, logger) + actorManager = actor.NewActorManager(clientset, logger.WithName("ActorManager")) + ) + + engine, err := ops.NewOpEngine(k8sClient, eventRecorder, actorManager, logger) + if err != nil { + panic(err) + } + + controllerReconciler := &DistributedRedisClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + EventRecorder: eventRecorder, + Engine: engine, + } + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + + err = k8sClient.Get(ctx, typeNamespacedName, inst) + Expect(err).NotTo(HaveOccurred()) + + logger.Info("Reconcile successfully", "instance", inst) + }) + }) +}) diff --git a/internal/controller/redis.kun/suite_test.go b/internal/controller/cluster/suite_test.go similarity index 69% rename from internal/controller/redis.kun/suite_test.go rename to internal/controller/cluster/suite_test.go index 83c616b..56e4b5e 100644 --- a/internal/controller/redis.kun/suite_test.go +++ b/internal/controller/cluster/suite_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,10 +17,14 @@ limitations under the License. package cluster import ( + "fmt" "path/filepath" + "runtime" + "testing" - . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -28,7 +32,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - rediskunv1alpha1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" + rediskunv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -39,6 +43,12 @@ var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) @@ -46,6 +56,14 @@ var _ = BeforeSuite(func() { testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), } var err error @@ -63,7 +81,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) -}, 60) +}) var _ = AfterSuite(func() { By("tearing down the test environment") diff --git a/internal/controller/databases.spotahome.com/failover_controller.go b/internal/controller/databases.spotahome.com/failover_controller.go deleted file mode 100644 index e32b9b9..0000000 --- a/internal/controller/databases.spotahome.com/failover_controller.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package failover - -import ( - "context" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/ops" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -type RedisFailoverReconciler struct { - client.Client - Scheme *runtime.Scheme - EventRecorder record.EventRecorder - Engine *ops.OpEngine -} - -// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redisfailovers,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redisfailovers/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redisfailovers/finalizers,verbs=update -func (r *RedisFailoverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx).WithValues("namespace", req.Namespace, "name", req.Name) - - var instance databasesv1.RedisFailover - if err := r.Get(ctx, req.NamespacedName, &instance); errors.IsNotFound(err) { - return ctrl.Result{}, nil - } else if err != nil { - logger.Error(err, "get resource failed") - return ctrl.Result{}, err - } - err := instance.Validate() - if err != nil { - instance.Status.Message = err.Error() - instance.Status.Phase = databasesv1.PhaseFail - if err := r.Status().Update(ctx, &instance); err != nil { - logger.Error(err, "update status failed") - return ctrl.Result{}, err - } - return ctrl.Result{}, err - } - - return r.Engine.Run(ctx, &instance) - -} - -func (r *RedisFailoverReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&databasesv1.RedisFailover{}). - WithEventFilter(predicate.GenerationChangedPredicate{}). - Owns(&appsv1.StatefulSet{}). - Owns(&corev1.Service{}). - Owns(&corev1.ConfigMap{}). - Owns(&corev1.Secret{}). - Owns(&appsv1.Deployment{}). - Complete(r) -} diff --git a/internal/controller/databases/redisfailover_controller.go b/internal/controller/databases/redisfailover_controller.go new file mode 100644 index 0000000..db885e8 --- /dev/null +++ b/internal/controller/databases/redisfailover_controller.go @@ -0,0 +1,198 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package databases + +import ( + "context" + "fmt" + "reflect" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/log" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + redisv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/ops" +) + +type RedisFailoverReconciler struct { + client.Client + Scheme *runtime.Scheme + EventRecorder record.EventRecorder + Engine *ops.OpEngine +} + +// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redisfailovers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redisfailovers/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redisfailovers/finalizers,verbs=update + +// Reconcile +func (r *RedisFailoverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithValues("namespace", req.Namespace, "name", req.Name) + + var instance databasesv1.RedisFailover + if err := r.Get(ctx, req.NamespacedName, &instance); errors.IsNotFound(err) { + return ctrl.Result{}, nil + } else if err != nil { + logger.Error(err, "get resource failed") + return ctrl.Result{}, err + } + err := instance.Validate() + if err != nil { + instance.Status.Message = err.Error() + instance.Status.Phase = databasesv1.Fail + if err := r.Status().Update(ctx, &instance); err != nil { + logger.Error(err, "update status failed") + return ctrl.Result{}, err + } + return ctrl.Result{}, err + } + + if crVersion := instance.Annotations[config.CRVersionKey]; crVersion == "" { + managedByRds := func() bool { + for _, ref := range instance.GetOwnerReferences() { + if ref.Kind == "Redis" { + return true + } + } + return false + }() + + if managedByRds { + logger.Info("no actor version specified, waiting for rds register rds version") + return ctrl.Result{RequeueAfter: time.Second * 15}, nil + } else if config.GetOperatorVersion() != "" { + instance.Annotations[config.CRVersionKey] = config.GetOperatorVersion() + if err := r.Client.Update(ctx, &instance); err != nil { + logger.Error(err, "update instance actor version failed") + } + return ctrl.Result{RequeueAfter: time.Second}, nil + } + } + + { + var ( + nodes []databasesv1.SentinelMonitorNode + serviceName = sentinelbuilder.GetSentinelHeadlessServiceName(instance.GetName()) + status = &instance.Status + oldStatus = instance.Status.DeepCopy() + ) + if instance.Spec.Sentinel == nil { + status.Monitor = databasesv1.MonitorStatus{ + Policy: databasesv1.ManualFailoverPolicy, + } + } else { + // TODO: use DNS SRV replace config all sentinel node address, which will cause data pods restart + for i := 0; i < int(instance.Spec.Sentinel.Replicas); i++ { + podName := sentinelbuilder.GetSentinelNodeServiceName(instance.GetName(), i) + srv := fmt.Sprintf("%s.%s.%s", podName, serviceName, instance.GetNamespace()) + nodes = append(nodes, databasesv1.SentinelMonitorNode{IP: srv, Port: 26379}) + } + + status.Monitor.Policy = databasesv1.SentinelFailoverPolicy + if instance.Spec.Sentinel.SentinelReference == nil { + // HARDCODE: use mymaster as sentinel monitor name + status.Monitor.Name = "mymaster" + + // append history password secrets + // NOTE: here recorded empty password + passwordSecret := instance.Spec.Sentinel.PasswordSecret + if status.Monitor.PasswordSecret != passwordSecret { + status.Monitor.OldPasswordSecret = status.Monitor.PasswordSecret + status.Monitor.PasswordSecret = passwordSecret + } + + if instance.Spec.Sentinel.EnableTLS { + if instance.Spec.Sentinel.ExternalTLSSecret != "" { + status.Monitor.TLSSecret = instance.Spec.Sentinel.ExternalTLSSecret + } else { + status.Monitor.TLSSecret = builder.GetRedisSSLSecretName(instance.GetName()) + } + } + status.Monitor.Nodes = nodes + } else { + status.Monitor.Name = fmt.Sprintf("%s.%s", instance.GetNamespace(), instance.GetName()) + status.Monitor.OldPasswordSecret = status.Monitor.PasswordSecret + status.Monitor.PasswordSecret = instance.Spec.Sentinel.SentinelReference.Auth.PasswordSecret + status.Monitor.TLSSecret = instance.Spec.Sentinel.SentinelReference.Auth.TLSSecret + status.Monitor.Nodes = instance.Spec.Sentinel.SentinelReference.Nodes + } + } + if !reflect.DeepEqual(status, oldStatus) { + if err := r.Status().Update(ctx, &instance); err != nil { + logger.Error(err, "update status failed") + return ctrl.Result{}, err + } + return ctrl.Result{RequeueAfter: time.Second}, nil + } + } + + var user redisv1.RedisUser + if err := r.Get(ctx, client.ObjectKey{ + Namespace: instance.Namespace, + Name: failoverbuilder.GenerateFailoverDefaultRedisUserName(instance.GetName()), + }, &user); err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "get user failed") + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + } else { + isUpdated := false + if secretName := instance.Spec.Auth.SecretPath; secretName != "" { + if len(user.Spec.PasswordSecrets) == 0 || user.Spec.PasswordSecrets[0] != secretName { + user.Spec.PasswordSecrets = append(user.Spec.PasswordSecrets[0:0], secretName) + isUpdated = true + } + } else { + if len(user.Spec.PasswordSecrets) != 0 { + user.Spec.PasswordSecrets = nil + isUpdated = true + } + } + if isUpdated { + if err := r.Update(ctx, &user); err != nil { + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + } + } + return r.Engine.Run(ctx, &instance) +} + +func (r *RedisFailoverReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&databasesv1.RedisFailover{}). + WithOptions(controller.Options{MaxConcurrentReconciles: 16}). + Owns(&databasesv1.RedisSentinel{}). + Owns(&appsv1.StatefulSet{}). + Owns(&corev1.Service{}). + Owns(&corev1.ConfigMap{}). + Owns(&corev1.Secret{}). + Owns(&appsv1.Deployment{}). + Complete(r) +} diff --git a/internal/controller/databases/redisfailover_controller_test.go b/internal/controller/databases/redisfailover_controller_test.go new file mode 100644 index 0000000..cb89b47 --- /dev/null +++ b/internal/controller/databases/redisfailover_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package databases + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" +) + +var _ = Describe("RedisFailover Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + redisfailover := &databasesv1.RedisFailover{} + + BeforeEach(func() { + By("creating the custom resource for the Kind RedisFailover") + err := k8sClient.Get(ctx, typeNamespacedName, redisfailover) + if err != nil && errors.IsNotFound(err) { + resource := &databasesv1.RedisFailover{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &databasesv1.RedisFailover{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance RedisFailover") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &RedisFailoverReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/controller/databases/redissentinel_controller.go b/internal/controller/databases/redissentinel_controller.go new file mode 100644 index 0000000..2bbbce9 --- /dev/null +++ b/internal/controller/databases/redissentinel_controller.go @@ -0,0 +1,77 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package databases + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/ops" +) + +// RedisSentinelReconciler reconciles a RedisSentinel object +type RedisSentinelReconciler struct { + client.Client + Scheme *runtime.Scheme + + EventRecorder record.EventRecorder + Engine *ops.OpEngine +} + +// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redissentinels,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redissentinels/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=databases.spotahome.com,resources=redissentinels/finalizers,verbs=update + +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.2/pkg/reconcile +func (r *RedisSentinelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(context.TODO()).WithValues("target", req.NamespacedName).WithName("RedisSentinel") + + var inst databasesv1.RedisSentinel + if err := r.Get(ctx, req.NamespacedName, &inst); err != nil { + if errors.IsNotFound(err) { + return reconcile.Result{}, nil + } + logger.Error(err, "failed to get RedisSentinel") + return reconcile.Result{}, err + } + return r.Engine.Run(ctx, &inst) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RedisSentinelReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&databasesv1.RedisSentinel{}). + // WithEventFilter(predicate.GenerationChangedPredicate{}). + WithOptions(controller.Options{MaxConcurrentReconciles: 8}). + Owns(&appsv1.StatefulSet{}). + Owns(&corev1.Service{}). + Owns(&corev1.ConfigMap{}). + Owns(&corev1.Secret{}). + Complete(r) +} diff --git a/internal/controller/databases/redissentinel_controller_test.go b/internal/controller/databases/redissentinel_controller_test.go new file mode 100644 index 0000000..faee933 --- /dev/null +++ b/internal/controller/databases/redissentinel_controller_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package databases + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" +) + +var _ = Describe("RedisSentinel Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + redissentinel := &databasesv1.RedisSentinel{} + + BeforeEach(func() { + By("creating the custom resource for the Kind RedisSentinel") + err := k8sClient.Get(ctx, typeNamespacedName, redissentinel) + if err != nil && errors.IsNotFound(err) { + resource := &databasesv1.RedisSentinel{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &databasesv1.RedisSentinel{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance RedisSentinel") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/controller/databases/suite_test.go b/internal/controller/databases/suite_test.go new file mode 100644 index 0000000..fa8efc6 --- /dev/null +++ b/internal/controller/databases/suite_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package databases + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.30.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = databasesv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/internal/controller/redis/utils/labels.go b/internal/controller/middleware/redis/helper.go similarity index 53% rename from internal/controller/redis/utils/labels.go rename to internal/controller/middleware/redis/helper.go index ec51731..96d21c4 100644 --- a/internal/controller/redis/utils/labels.go +++ b/internal/controller/middleware/redis/helper.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -package utils +package redis -const ( - InstanceTypeLabel = "middleware.instance/type" - InstanceNameLabel = "middleware.instance/name" +import "github.com/alauda/redis-operator/internal/builder" +const ( RedisFailoverType = "redis-failover" RedisClusterType = "distributed-redis-cluster" ) @@ -28,34 +27,27 @@ func GetRedisSentinelLabels(instanceName, failoverName string) map[string]string return map[string]string{ "app.kubernetes.io/name": failoverName, "app.kubernetes.io/part-of": RedisFailoverType, - InstanceTypeLabel: RedisFailoverType, - InstanceNameLabel: instanceName, + builder.InstanceTypeLabel: RedisFailoverType, + builder.InstanceNameLabel: instanceName, } } func GetRedisClusterLabels(instanceName, clusterName string) map[string]string { return map[string]string{ - "managed-by": "redis-cluster-operator", - "redis.kun/name": clusterName, - InstanceTypeLabel: RedisClusterType, - InstanceNameLabel: instanceName, + "managed-by": "redis-cluster-operator", + "redis.kun/name": clusterName, + builder.InstanceTypeLabel: RedisClusterType, + builder.InstanceNameLabel: instanceName, } } -func GetRedisRdsLabels(instanceName string) map[string]string { - return map[string]string{ - "app.kubernetes.io/name": instanceName, - "app.kubernetes.io/component": "redis", - "app.kubernetes.io/part-of": "middleware-rds", - } +func GetRedisFailoverName(instanceName string) string { + return instanceName +} +func GetRedisClusterName(instanceName string) string { + return instanceName } -func MergeLabels(vals ...map[string]string) map[string]string { - ret := map[string]string{} - for _, item := range vals { - for k, v := range item { - ret[k] = v - } - } - return ret +func GetRedisStorageVolumeName(instanceName string) string { + return "redis-data" } diff --git a/internal/controller/middleware/redis/pvcs.go b/internal/controller/middleware/redis/pvcs.go new file mode 100644 index 0000000..286e085 --- /dev/null +++ b/internal/controller/middleware/redis/pvcs.go @@ -0,0 +1,79 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package redis + +import ( + "context" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // storage + StorageProvisionerKey = "volume.beta.kubernetes.io/storage-provisioner" + TopoLVMProvisionerKey = "topolvm.cybozu.com" +) + +func GetShardMaxPVCQuantity(ctx context.Context, client ctrlClient.Client, namespace string, labels map[string]string) (*resource.Quantity, error) { + var maxQuantity = resource.NewQuantity(0, resource.BinarySI) + pvcs := &v1.PersistentVolumeClaimList{} + if err := client.List(ctx, pvcs, ctrlClient.InNamespace(namespace), ctrlClient.MatchingLabels(labels)); err != nil { + return nil, err + } + for _, pvc := range pvcs.Items { + if pvc.Status.Phase != v1.ClaimBound { + continue + } + if pvc.Spec.Resources.Requests.Storage().Cmp(*maxQuantity) > 0 { + maxQuantity = pvc.Spec.Resources.Requests.Storage() + } + } + return maxQuantity, nil +} + +func ResizePVCs(ctx context.Context, client ctrlClient.Client, namespace string, labels map[string]string, storageQuantity resource.Quantity) error { + if labels == nil { + return nil + } + pvcs := &v1.PersistentVolumeClaimList{} + if err := client.List(ctx, pvcs, ctrlClient.InNamespace(namespace), ctrlClient.MatchingLabels(labels)); err != nil { + return err + } + + for _, pvc := range pvcs.Items { + newPVC := pvc.DeepCopy() + // determine whether the storage class support expand + if newPVC.Annotations[StorageProvisionerKey] != TopoLVMProvisionerKey { + continue + } + // wait pvc to bound + if pvc.Status.Phase != v1.ClaimBound { + continue + } + // compare storage size + if pvc.Spec.Resources.Requests.Storage().Equal(storageQuantity) { + continue + } + newPVC.Spec.Resources.Requests[v1.ResourceStorage] = storageQuantity + if err := client.Update(ctx, newPVC); err != nil { + return err + } + } + return nil +} diff --git a/internal/controller/middleware/redis/rediscluster.go b/internal/controller/middleware/redis/rediscluster.go new file mode 100644 index 0000000..8cc5c08 --- /dev/null +++ b/internal/controller/middleware/redis/rediscluster.go @@ -0,0 +1,426 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package redis + +import ( + "reflect" + "strings" + "time" + + v1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + v1 "github.com/alauda/redis-operator/api/middleware/v1" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/internal/vc" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func GetRedisVersion(image string) string { + switch { + case strings.Contains(image, "redis:4.0"): + return "4.0" + case strings.Contains(image, "redis:5.0"): + return "5.0" + case strings.Contains(image, "redis:6.0"): + return "6.0" + } + return "" +} + +type ApplyPolicy string + +const ( + Unsupported ApplyPolicy = "Unsupported" + RestartApply ApplyPolicy = "RestartApply" +) + +func GetRedisConfigsApplyPolicyByVersion(ver string) map[string]ApplyPolicy { + data := map[string]ApplyPolicy{ + "databases": RestartApply, + "rename-command": RestartApply, + "rdbchecksum": RestartApply, + "tcp-backlog": RestartApply, + "io-threads": RestartApply, + "io-threads-do-reads": RestartApply, + } + rv := redis.RedisVersion(ver) + if rv.IsACLSupported() { + delete(data, "rename-command") + } + return data +} + +const ( + ControllerVersionLabel = "controllerVersion" +) + +func GenerateRedisCluster(instance *v1.Redis, bv *vc.BundleVersion) (*v1alpha1.DistributedRedisCluster, error) { + customConfig := instance.Spec.CustomConfig + if customConfig == nil { + customConfig = map[string]string{} + } + if instance.Spec.PodAnnotations == nil { + instance.Spec.PodAnnotations = map[string]string{} + } + + image, err := bv.GetRedisImage(instance.Spec.Version) + if instance.Spec.EnableActiveRedis { + image, err = bv.GetActiveRedisImage(instance.Spec.Version) + } + if err != nil { + return nil, err + } + + labels := GetRedisClusterLabels(instance.Name, GetRedisClusterName(instance.Name)) + if v := instance.Labels[ControllerVersionLabel]; v != "" { + labels[ControllerVersionLabel] = v + } + + var ( + podSec = instance.Spec.SecurityContext + containerSec *corev1.SecurityContext + access = core.InstanceAccess{ + InstanceAccessBase: *instance.Spec.Expose.InstanceAccessBase.DeepCopy(), + IPFamilyPrefer: instance.Spec.IPFamilyPrefer, + } + backup = instance.Spec.Backup + restore = instance.Spec.Restore + annotations = map[string]string{} + ) + for key, comp := range bv.Spec.Components { + if key == "redis" { + annotations[config.BuildImageVersionKey("redis")] = image + } else if len(comp.ComponentVersions) > 0 { + annotations[config.BuildImageVersionKey(key)], _ = bv.GetImage(key, "") + } + } + + access.Image, _ = bv.GetRedisToolsImage() + if len(backup.Schedule) > 0 { + backup.Image, _ = bv.GetRedisToolsImage() + } + if restore.BackupName != "" { + restore.Image, _ = bv.GetRedisToolsImage() + } + if instance.Status.Restored { + restore = core.RedisRestore{} + } + + exporterImage, _ := bv.GetRedisExporterImage() + monitor := &v1alpha1.Monitor{ + Image: exporterImage, + Resources: &corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("300Mi"), + }, + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("300Mi"), + }, + }, + } + + if podSec != nil { + containerSec = &corev1.SecurityContext{ + SELinuxOptions: podSec.SELinuxOptions, + WindowsOptions: podSec.WindowsOptions, + RunAsUser: podSec.RunAsUser, + RunAsGroup: podSec.RunAsGroup, + RunAsNonRoot: podSec.RunAsNonRoot, + SeccompProfile: podSec.SeccompProfile, + } + } + + shards := instance.Spec.Replicas.Cluster.Shards + if *instance.Spec.Replicas.Cluster.Shard != int32(len(shards)) { + shards = nil + } + + cluster := &v1alpha1.DistributedRedisCluster{ + ObjectMeta: v12.ObjectMeta{ + Name: GetRedisClusterName(instance.Name), + Namespace: instance.Namespace, + Labels: labels, + Annotations: annotations, + OwnerReferences: util.BuildOwnerReferences(instance), + }, + Spec: v1alpha1.DistributedRedisClusterSpec{ + Image: image, + MasterSize: *instance.Spec.Replicas.Cluster.Shard, + ClusterReplicas: *instance.Spec.Replicas.Cluster.Slave, + Resources: instance.Spec.Resources, + Config: customConfig, + Shards: shards, + Affinity: instance.Spec.Affinity, + AffinityPolicy: instance.Spec.AffinityPolicy, + NodeSelector: instance.Spec.NodeSelector, + Tolerations: instance.Spec.Tolerations, + SecurityContext: instance.Spec.SecurityContext, + ContainerSecurityContext: containerSec, + PodAnnotations: instance.Spec.PodAnnotations, + IPFamilyPrefer: instance.Spec.IPFamilyPrefer, + EnableTLS: instance.Spec.EnableTLS, + EnableActiveRedis: instance.Spec.EnableActiveRedis, + ServiceID: instance.Spec.ServiceID, + // with custom images + Expose: access, + Backup: backup, + Restore: restore, + Monitor: monitor, + }, + } + // add password secret + if !instance.PasswordIsEmpty() { + cluster.Spec.PasswordSecret = &corev1.LocalObjectReference{Name: instance.Status.PasswordSecretName} + } else { + cluster.Spec.PasswordSecret = nil + } + + if instance.Spec.Persistent != nil && instance.Spec.Persistent.StorageClassName != "" && + (instance.Spec.PersistentSize == nil || instance.Spec.PersistentSize.IsZero()) { + size := resource.NewQuantity(instance.Spec.Resources.Limits.Memory().Value()*2, resource.BinarySI) + instance.Spec.PersistentSize = size + } + sc := "" + if instance.Spec.Persistent != nil { + sc = instance.Spec.Persistent.StorageClassName + } + if size := instance.Spec.PersistentSize; size != nil { + cluster.Spec.Storage = &v1alpha1.RedisStorage{ + Type: v1alpha1.PersistentClaim, + Size: *size, + Class: sc, + DeleteClaim: false, + } + } + return cluster, nil +} + +func ShouldUpdateCluster(cluster, newCluster *v1alpha1.DistributedRedisCluster, logger logr.Logger) bool { + if newCluster.Spec.EnableActiveRedis != cluster.Spec.EnableActiveRedis || + (newCluster.Spec.ServiceID == nil && cluster.Spec.ServiceID != nil) || + (newCluster.Spec.ServiceID != nil && cluster.Spec.ServiceID == nil) || + ((newCluster.Spec.ServiceID != nil && cluster.Spec.ServiceID != nil) && + (*newCluster.Spec.ServiceID != *cluster.Spec.ServiceID)) { + return true + } + + if !reflect.DeepEqual(cluster.Labels, newCluster.Labels) || + !reflect.DeepEqual(cluster.Annotations, newCluster.Annotations) { + return true + } + + if cluster.Spec.Image != newCluster.Spec.Image { + return true + } + if resourceDiff(*newCluster.Spec.Resources, *cluster.Spec.Resources) { + logger.V(3).Info("cluster resources diff") + return true + } + if !reflect.DeepEqual(cluster.Spec.PasswordSecret, newCluster.Spec.PasswordSecret) { + logger.V(3).Info("cluster password diff") + return true + } + if newCluster.Spec.MasterSize != cluster.Spec.MasterSize { + logger.V(3).Info("cluster replicas diff") + return true + } + if newCluster.Spec.ClusterReplicas != cluster.Spec.ClusterReplicas { + logger.V(3).Info("cluster slave diff") + return true + } + if !reflect.DeepEqual(newCluster.Spec.Config, cluster.Spec.Config) { + logger.V(3).Info("cluster customconfig diff") + return true + } + + if diffAnnonation(newCluster.Spec.PodAnnotations, cluster.Spec.PodAnnotations) { + logger.V(3).Info("pod annotations diff") + return true + } + if diffBackup(&cluster.Spec.Backup, &newCluster.Spec.Backup) { + logger.V(3).Info("redis backup diff") + return true + } + if !reflect.DeepEqual(&cluster.Spec.Restore, &newCluster.Spec.Restore) { + logger.V(3).Info("redis restore diff") + return true + } + + if !reflect.DeepEqual(cluster.Spec.Expose, newCluster.Spec.Expose) { + logger.V(3).Info("redis expose diff") + return true + } + + if cluster.Spec.AffinityPolicy != newCluster.Spec.AffinityPolicy || + !reflect.DeepEqual(cluster.Spec.NodeSelector, newCluster.Spec.NodeSelector) || + !reflect.DeepEqual(cluster.Spec.Tolerations, newCluster.Spec.Tolerations) { + return true + } + if !reflect.DeepEqual(cluster.Spec.SecurityContext, newCluster.Spec.SecurityContext) { + return true + } + + storage1 := cluster.Spec.Storage + storage2 := newCluster.Spec.Storage + if storage1 != nil && storage2 != nil { + val1, _ := storage1.Size.AsInt64() + val2, _ := storage2.Size.AsInt64() + if val1 != val2 { + return true + } + } else if storage1 != nil || storage2 != nil { + return true + } + return false +} + +func GenerateClusterRedisByManagerUI(cluster *v1alpha1.DistributedRedisCluster, scheme *runtime.Scheme, secret *corev1.Secret) *v1.Redis { + instance := &v1.Redis{ + ObjectMeta: v12.ObjectMeta{ + Name: cluster.Name, + Namespace: cluster.Namespace, + Labels: cluster.Labels, + Annotations: map[string]string{"createType": "managerView"}, + }, + Spec: v1.RedisSpec{ + Arch: core.RedisCluster, + Version: GetRedisVersion(cluster.Spec.Image), + Resources: cluster.Spec.Resources, + Replicas: &v1.RedisReplicas{ + Cluster: &v1.ClusterReplicas{ + Shard: &cluster.Spec.MasterSize, + Slave: &cluster.Spec.ClusterReplicas, + }, + }, + Backup: cluster.Spec.Backup, + Restore: cluster.Spec.Restore, + Affinity: cluster.Spec.Affinity, + NodeSelector: cluster.Spec.NodeSelector, + Tolerations: cluster.Spec.Tolerations, + SecurityContext: cluster.Spec.SecurityContext, + PodAnnotations: cluster.Spec.PodAnnotations, + CustomConfig: cluster.Spec.Config, + // Monitor: cluster.Spec.Monitor, + EnableTLS: cluster.Spec.EnableTLS, + }, + } + + // Persistent: + // PersistentSize: + if cluster.Spec.Storage != nil { + instance.Spec.Persistent = &v1.RedisPersistent{ + StorageClassName: cluster.Spec.Storage.Class, + } + instance.Spec.PersistentSize = &cluster.Spec.Storage.Size + } + // Password: + // PasswordSecret: + if secret != nil { + instance.Spec.PasswordSecret = secret.Name + } + // label + instance.Labels = make(map[string]string) + for k, v := range GetRedisClusterLabels(instance.Name, GetRedisClusterName(instance.Name)) { + instance.Labels[k] = v + } + return instance +} + +func ClusterIsUp(cluster *v1alpha1.DistributedRedisCluster) bool { + return cluster.Status.Status == v1alpha1.ClusterStatusOK && + cluster.Status.ClusterStatus == v1alpha1.ClusterInService +} + +const ( + redisRestartAnnotation = "kubectl.kubernetes.io/restartedAt" + PauseAnnotationKey = "app.cpaas.io/pause-timestamp" +) + +func diffAnnonation(rfAnnotations map[string]string, targetAnnotations map[string]string) bool { + if len(rfAnnotations) == 0 { + if len(targetAnnotations) > 0 && targetAnnotations[PauseAnnotationKey] != "" { + return true + } + return false + } else { + if len(targetAnnotations) > 0 && targetAnnotations[PauseAnnotationKey] != rfAnnotations[PauseAnnotationKey] { + return true + } + } + if len(targetAnnotations) == 0 { + return true + } + for k, v := range rfAnnotations { + if k == redisRestartAnnotation { + if v == "" { + continue + } + targetV := targetAnnotations[redisRestartAnnotation] + if targetV == "" { + return true + } + newTime, err1 := time.Parse(time.RFC3339Nano, v) + targetTime, err2 := time.Parse(time.RFC3339Nano, targetV) + if err1 != nil || err2 != nil { + return true + } + if newTime.After(targetTime) { + return true + } + } else if targetAnnotations[k] != v { + return true + } + } + return false +} + +func MergeAnnotations(t, s map[string]string) map[string]string { + if t == nil { + return s + } + if s == nil { + return t + } + + for k, v := range s { + if k == redisRestartAnnotation { + tRestartAnn := t[k] + if tRestartAnn == "" && v != "" { + t[k] = v + } + + tTime, err1 := time.Parse(time.RFC3339Nano, tRestartAnn) + sTime, err2 := time.Parse(time.RFC3339Nano, v) + if err1 != nil || err2 != nil || sTime.After(tTime) { + t[k] = v + } else { + t[k] = tRestartAnn + } + } else { + t[k] = v + } + } + return t +} diff --git a/internal/controller/middleware/redis/redisfailover.go b/internal/controller/middleware/redis/redisfailover.go new file mode 100644 index 0000000..81989a2 --- /dev/null +++ b/internal/controller/middleware/redis/redisfailover.go @@ -0,0 +1,377 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package redis + +import ( + "reflect" + "strings" + + "github.com/go-logr/logr" + "github.com/samber/lo" + + "github.com/alauda/redis-operator/api/core" + redisfailover "github.com/alauda/redis-operator/api/databases/v1" + v1 "github.com/alauda/redis-operator/api/middleware/v1" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/internal/vc" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GenerateRedisFailover(instance *v1.Redis, bv *vc.BundleVersion) (*redisfailover.RedisFailover, error) { + image, err := bv.GetRedisImage(instance.Spec.Version) + if instance.Spec.EnableActiveRedis { + image, err = bv.GetActiveRedisImage(instance.Spec.Version) + } + if err != nil { + return nil, err + } + + if instance.Spec.Arch == core.RedisSentinel { + if instance.Spec.Sentinel == nil { + instance.Spec.Sentinel = &redisfailover.SentinelSettings{} + } + } else { + instance.Spec.Sentinel = nil + } + + var ( + access = core.InstanceAccess{ + InstanceAccessBase: *instance.Spec.Expose.InstanceAccessBase.DeepCopy(), + IPFamilyPrefer: instance.Spec.IPFamilyPrefer, + } + exporter = instance.Spec.Exporter.DeepCopy() + backup = instance.Spec.Backup + restore = instance.Spec.Restore + sentinel = instance.Spec.Sentinel + + annotations = map[string]string{} + ) + + for key, comp := range bv.Spec.Components { + if key == "redis" { + annotations[config.BuildImageVersionKey("redis")] = image + } else if len(comp.ComponentVersions) > 0 { + annotations[config.BuildImageVersionKey(key)], _ = bv.GetImage(key, "") + } + } + if exporter == nil { + exporter = &redisfailover.RedisExporter{Enabled: true} + } + exporter.Image, _ = bv.GetRedisExporterImage() + if exporter.Resources.Limits.Cpu().IsZero() || exporter.Resources.Limits.Memory().IsZero() { + exporter.Resources = corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("384Mi"), + }, + } + } + + access.Image, _ = bv.GetRedisToolsImage() + // HARDCODE: compatible with 3.14.x + if strings.HasPrefix(bv.Spec.CrVersion, "3.14.") { + access.Image, _ = bv.GetExposePodImage() + } + + if len(backup.Schedule) > 0 { + backup.Image, _ = bv.GetRedisToolsImage() + } + if restore.BackupName != "" { + restore.Image, _ = bv.GetRedisToolsImage() + } + if instance.Status.Restored { + restore = core.RedisRestore{} + } + + if sentinel != nil { + if sentinel.SentinelReference == nil { + sentinel.Image = image + sentinel.Expose.AccessPort = instance.Spec.Expose.AccessPort + sentinel.Expose.Image, _ = bv.GetRedisToolsImage() + sentinel.Expose.ServiceType = access.ServiceType + sentinel.Expose.IPFamilyPrefer = instance.Spec.IPFamilyPrefer + sentinel.EnableTLS = instance.Spec.EnableTLS + + if len(sentinel.NodeSelector) == 0 { + sentinel.NodeSelector = instance.Spec.NodeSelector + } + if sentinel.Tolerations == nil { + sentinel.Tolerations = instance.Spec.Tolerations + } + if sentinel.SecurityContext == nil { + sentinel.SecurityContext = instance.Spec.SecurityContext + } + sentinel.PodAnnotations = lo.Assign(sentinel.PodAnnotations, instance.Spec.PodAnnotations) + } + } else { + annotations["standalone"] = "true" + if !instance.Status.Restored { + // TRICK: used to migrate from devops old redis versions, included keys: + // * redis-standalone/storage-type - storage type, pvc or hostpath + // * redis-standalone/filepath + // * redis-standalone/pvc-name + // * redis-standalone/hostpath + // TODO: remove in acp 3.22 + for k, v := range instance.Annotations { + if strings.HasPrefix(k, "redis-standalone") { + annotations[k] = v + } + } + } + } + + failover := &redisfailover.RedisFailover{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetRedisFailoverName(instance.Name), + Namespace: instance.Namespace, + Labels: GetRedisSentinelLabels(instance.Name, GetRedisFailoverName(instance.Name)), + Annotations: annotations, + OwnerReferences: util.BuildOwnerReferences(instance), + }, + Spec: redisfailover.RedisFailoverSpec{ + Redis: redisfailover.RedisSettings{ + Image: image, + Replicas: int32(*instance.Spec.Replicas.Sentinel.Slave) + 1, + Resources: *instance.Spec.Resources, + CustomConfig: instance.Spec.CustomConfig, + PodAnnotations: lo.Assign(instance.Spec.PodAnnotations), + Exporter: *exporter, + Backup: backup, + Restore: restore, + Expose: access, + EnableTLS: instance.Spec.EnableTLS, + + Affinity: instance.Spec.Affinity, + NodeSelector: instance.Spec.NodeSelector, + Tolerations: instance.Spec.Tolerations, + SecurityContext: instance.Spec.SecurityContext, + }, + Auth: redisfailover.AuthSettings{ + SecretPath: instance.Spec.PasswordSecret, + }, + Sentinel: sentinel, + EnableActiveRedis: instance.Spec.EnableActiveRedis, + ServiceID: instance.Spec.ServiceID, + }, + } + + if instance.Spec.Persistent != nil && instance.Spec.Persistent.StorageClassName != "" && + (instance.Spec.PersistentSize == nil || instance.Spec.PersistentSize.IsZero()) { + size := resource.NewQuantity(instance.Spec.Resources.Limits.Memory().Value()*2, resource.BinarySI) + instance.Spec.PersistentSize = size + } + var sc *string + if instance.Spec.Persistent != nil && instance.Spec.Persistent.StorageClassName != "" { + sc = &instance.Spec.Persistent.StorageClassName + } + if size := instance.Spec.PersistentSize; size != nil { + failover.Spec.Redis.Storage.KeepAfterDeletion = true + failover.Spec.Redis.Storage.PersistentVolumeClaim = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetRedisStorageVolumeName(instance.Name), + Labels: GetRedisSentinelLabels(instance.Name, failover.Name), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceStorage: *size}, + }, + StorageClassName: sc, + }, + } + } + return failover, nil +} + +func ShouldUpdateFailover(failover, newFailover *redisfailover.RedisFailover, logger logr.Logger) bool { + if newFailover.Spec.EnableActiveRedis != failover.Spec.EnableActiveRedis || + (newFailover.Spec.ServiceID == nil && failover.Spec.ServiceID != nil) || + (newFailover.Spec.ServiceID != nil && failover.Spec.ServiceID == nil) || + ((newFailover.Spec.ServiceID != nil && failover.Spec.ServiceID != nil) && + (*newFailover.Spec.ServiceID != *failover.Spec.ServiceID)) { + return true + } + + if diffAnnonation(newFailover.Annotations, failover.Annotations) || + !reflect.DeepEqual(newFailover.Labels, failover.Labels) { + return true + } + if !reflect.DeepEqual(newFailover.Spec.Redis, failover.Spec.Redis) || + !reflect.DeepEqual(failover.Spec.Sentinel, newFailover.Spec.Sentinel) { + return true + } + if !reflect.DeepEqual(failover.Spec.Redis.Expose, newFailover.Spec.Redis.Expose) { + return true + } + if failover.Spec.Redis.EnableTLS != newFailover.Spec.Redis.EnableTLS || + failover.Spec.Auth.SecretPath != newFailover.Spec.Auth.SecretPath { + logger.V(3).Info("failover secrepath diff") + return true + } + + newPvc := newFailover.Spec.Redis.Storage.PersistentVolumeClaim + pvc := failover.Spec.Redis.Storage.PersistentVolumeClaim + if newPvc != nil && pvc != nil { + val1, _ := pvc.Spec.Resources.Requests.Storage().AsInt64() + val2, _ := newPvc.Spec.Resources.Requests.Storage().AsInt64() + if val1 != val2 { + return true + } + } else if newPvc != nil || pvc != nil { + return true + } + return false +} + +func GenerateFailoverRedisByManagerUI(failover *redisfailover.RedisFailover, sts *appsv1.StatefulSet, secret *corev1.Secret) *v1.Redis { + var redisContainer corev1.Container + for _, v := range sts.Spec.Template.Spec.Containers { + if v.Name == "redis" { + redisContainer = v + } + } + var master int32 = 1 + var slave int32 + if *sts.Spec.Replicas == 0 { + slave = 0 + } else { + slave = *sts.Spec.Replicas - 1 + } + + instance := &v1.Redis{ + ObjectMeta: metav1.ObjectMeta{ + Name: failover.Name, + Namespace: failover.Namespace, + Labels: failover.Labels, + Annotations: map[string]string{"createType": "managerView"}, + }, + Spec: v1.RedisSpec{ + Version: GetRedisVersion(redisContainer.Image), + Arch: core.RedisSentinel, + Resources: &redisContainer.Resources, + Persistent: &v1.RedisPersistent{}, + //PersistentSize: nil, + //Password: nil, + //PasswordSecret: "", + Replicas: &v1.RedisReplicas{ + Sentinel: &v1.SentinelReplicas{ + Master: &master, + Slave: &slave, + }, + }, + Backup: failover.Spec.Redis.Backup, + Restore: failover.Spec.Redis.Restore, + Affinity: failover.Spec.Redis.Affinity, + NodeSelector: failover.Spec.Redis.NodeSelector, + Tolerations: failover.Spec.Redis.Tolerations, + SecurityContext: failover.Spec.Redis.SecurityContext, + CustomConfig: failover.Spec.Redis.CustomConfig, + PodAnnotations: failover.Spec.Redis.PodAnnotations, + Sentinel: failover.Spec.Sentinel, + SentinelCustomConfig: failover.Spec.Sentinel.CustomConfig, + Exporter: &failover.Spec.Redis.Exporter, + EnableTLS: failover.Spec.Redis.EnableTLS, + PasswordSecret: failover.Spec.Auth.SecretPath, + }, + } + + //Persistent、PersistentSize + if failover.Spec.Redis.Storage.PersistentVolumeClaim != nil { + instance.Spec.Persistent.StorageClassName = *failover.Spec.Redis.Storage.PersistentVolumeClaim.Spec.StorageClassName + instance.Spec.PersistentSize = failover.Spec.Redis.Storage.PersistentVolumeClaim.Spec.Resources.Requests.Storage() + } + + //Password PasswordSecret + if secret != nil { + instance.Spec.PasswordSecret = secret.Name + } + + // label + if len(instance.Labels) == 0 { + instance.Labels = make(map[string]string) + } + for k, v := range GetRedisSentinelLabels(instance.Name, GetRedisFailoverName(instance.Name)) { + instance.Labels[k] = v + } + return instance +} + +func diffBackup(b1, b2 *core.RedisBackup) bool { + if b1 == nil && b2 == nil { + return false + } + if (b1 == nil && b2 != nil) || (b1 != nil && b2 == nil) { + return true + } + + if b1.Image != b2.Image { + return true + } + if len(b1.Schedule) != len(b2.Schedule) { + return true + } else if len(b1.Schedule) > 0 { + for i, s := range b1.Schedule { + ss := b2.Schedule[i] + if s.Schedule != ss.Schedule { + return true + } + if s.Name != ss.Name { + return true + } + if s.Keep != ss.Keep { + return true + } + if s.KeepAfterDeletion != ss.KeepAfterDeletion { + return true + } + if s.Storage.StorageClassName != ss.Storage.StorageClassName { + return true + } + if s.Storage.Size.Size() != ss.Storage.Size.Size() { + return true + } + if !reflect.DeepEqual(s.Target.S3Option, ss.Target.S3Option) { + return true + } + } + } + return false +} + +func resourceDiff(r1 corev1.ResourceRequirements, r2 corev1.ResourceRequirements) bool { + if result := r1.Requests.Cpu().Cmp(*r2.Requests.Cpu()); result != 0 { + return true + } + if result := r1.Requests.Memory().Cmp(*r2.Requests.Memory()); result != 0 { + return true + } + if result := r1.Limits.Memory().Cmp(*r2.Limits.Memory()); result != 0 { + return true + } + if result := r1.Limits.Memory().Cmp(*r2.Limits.Memory()); result != 0 { + return true + } + return false +} diff --git a/internal/controller/middleware/redis_controller.go b/internal/controller/middleware/redis_controller.go new file mode 100644 index 0000000..3ce77ad --- /dev/null +++ b/internal/controller/middleware/redis_controller.go @@ -0,0 +1,907 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + redisfailover "github.com/alauda/redis-operator/api/databases/v1" + rdsv1 "github.com/alauda/redis-operator/api/middleware/v1" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/controller/middleware/redis" + redissvc "github.com/alauda/redis-operator/internal/controller/middleware/redis" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/internal/vc" + "github.com/alauda/redis-operator/pkg/actor" + + "github.com/go-logr/logr" + "github.com/samber/lo" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// RedisReconciler reconciles a Redis object +type RedisReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger + ActorManager *actor.ActorManager +} + +const ( + createRedisSentinel = "~create-redis-sentinel" + createRedisCluster = "~create-redis-cluster" + pvcFinalizer = "delete-pvc" + requeueSecond = 10 * time.Second + DisableRdsManagementAnnotationKey = "cpaas.io/disable-rds-management" +) + +//+kubebuilder:rbac:groups=middleware.alauda.io,resources=redis,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=middleware.alauda.io,resources=redis/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=middleware.alauda.io,resources=redis/finalizers,verbs=update +//+kubebuilder:rbac:groups=middleware.alauda.io,resources=imageversions,verbs=get;list;watch;create;update;patch;delete + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(context.TODO()).WithValues("target", req.NamespacedName).WithName("RDS") + + if strings.Contains(req.NamespacedName.Name, createRedisSentinel) { + //引发同步的是管理视图的哨兵模式的redis创建 + return ctrl.Result{}, r.createRedisByManagerUICreatedWithSentinel(ctx, req.NamespacedName) + } else if strings.Contains(req.NamespacedName.Name, createRedisCluster) { + //引发同步的是管理视图的集群模式的redis创建 + return ctrl.Result{}, r.createRedisByManagerUICreatedWithCluster(ctx, req.NamespacedName) + } + + inst := &rdsv1.Redis{} + if err := r.Get(ctx, req.NamespacedName, inst); err != nil { + if errors.IsNotFound(err) { + return reconcile.Result{}, nil + } + logger.Error(err, "Fail to get redis") + return reconcile.Result{}, err + } + if inst.GetDeletionTimestamp() != nil { + if err := r.processFinalizer(inst); err != nil { + logger.Error(err, "fail to process finalizer") + return r.updateInstanceStatus(ctx, inst, err, logger) + } + return ctrl.Result{}, nil + } + + oldInst := inst.DeepCopy() + inst.Default() + if !reflect.DeepEqual(oldInst, inst) { + if err := r.Update(ctx, inst); err != nil { + logger.Error(err, "fail to update redis") + return ctrl.Result{}, err + } + } + + if inst.Annotations[config.CRAutoUpgradeKey] == "false" && + (inst.Spec.UpgradeOption.AutoUpgrade == nil || *inst.Spec.UpgradeOption.AutoUpgrade) { + // patch autoUpgrade + operatorVersion := inst.Annotations["operatorVersion"] + if strings.HasPrefix(operatorVersion, "v3.14.") || + strings.HasPrefix(operatorVersion, "v3.15.") || + strings.HasPrefix(operatorVersion, "v3.16.") { + ver, err := semver.NewVersion(operatorVersion) + if err != nil { + logger.Error(err, "invalid operator version", "operatorVersion", operatorVersion) + if _, err := r.updateInstanceStatus(ctx, inst, err, logger); err != nil { + logger.Error(err, "fail to update redis instance status") + } + return ctrl.Result{}, nil + } + + inst.Spec.UpgradeOption.AutoUpgrade = pointer.Bool(false) + inst.Spec.UpgradeOption.CRVersion = ver.String() + if _, err := r.updateInstance(ctx, inst, logger); err != nil { + logger.Error(err, "fail to update redis instance") + } + inst.Status.UpgradeStatus.CRVersion = ver.String() + if _, err := r.updateInstanceStatus(ctx, inst, nil, logger); err != nil { + logger.Error(err, "fail to update redis instance status") + } + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + } + + var ( + err error + bv *vc.BundleVersion + operatorVersion = config.GetOperatorVersion() + ) + + if inst.Spec.UpgradeOption == nil || inst.Spec.UpgradeOption.AutoUpgrade == nil || *inst.Spec.UpgradeOption.AutoUpgrade { + // NOTE: upgrade to current version no matter what current version is + // which means if current redis version is 6.0.20, if the BundleVersion contains a redis with 6.0.19 + // it will do upgrade anyway + if bv, err = vc.GetLatestBundleVersion(ctx, r.Client); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "fail to get latest bundle version", "operatorVersion", operatorVersion) + return r.updateInstanceStatus(ctx, inst, err, logger) + } + if bv == nil { + if bv, err = vc.GetBundleVersion(ctx, r.Client, inst.Status.UpgradeStatus.CRVersion); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "fail to get bundle version", "operatorVersion", operatorVersion) + return r.updateInstanceStatus(ctx, inst, err, logger) + } + } + } else { + var latestBV *vc.BundleVersion + if inst.Spec.UpgradeOption.CRVersion != "" { + if bv, err = vc.GetBundleVersion(ctx, r.Client, inst.Spec.UpgradeOption.CRVersion); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "fail to get bundle version", "operatorVersion", operatorVersion) + return r.updateInstanceStatus(ctx, inst, err, logger) + } + } else if inst.Status.UpgradeStatus.CRVersion != "" { + if bv, err = vc.GetBundleVersion(ctx, r.Client, inst.Status.UpgradeStatus.CRVersion); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "fail to get bundle version", "operatorVersion", operatorVersion) + return r.updateInstanceStatus(ctx, inst, err, logger) + } + } else { + if latestBV, err = vc.GetLatestBundleVersion(ctx, r.Client); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "fail to get latest bundle version", "operatorVersion", operatorVersion) + return r.updateInstanceStatus(ctx, inst, err, logger) + } + bv = latestBV + } + + if inst.Status.UpgradeStatus.CRVersion != "" { + if latestBV == nil { + if latestBV, err = vc.GetLatestBundleVersion(ctx, r.Client); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "fail to get latest bundle version", "operatorVersion", operatorVersion) + return r.updateInstanceStatus(ctx, inst, err, logger) + } + } + if latestBV != nil && + latestBV.Spec.CrVersion != inst.Status.UpgradeStatus.CRVersion && + latestBV.Spec.CrVersion != inst.Annotations[config.CRUpgradeableVersion] { + if vc.IsBundleVersionUpgradeable(bv, latestBV, inst.Spec.Version) { + comp := latestBV.GetComponent(config.CoreComponentName, inst.Spec.Version) + inst.Annotations[config.CRUpgradeableVersion] = latestBV.Spec.CrVersion + inst.Annotations[config.CRUpgradeableComponentVersion] = comp.Version + + if _, err := r.updateInstance(ctx, inst, logger); err != nil { + logger.Error(err, "fail to update redis instance") + } + } + } + } + } + if bv == nil { + err := fmt.Errorf("bundle version not found") + logger.Error(err, "fail to get any usable ImageVersion") + return r.updateInstanceStatus(ctx, inst, err, logger) + } + + if operatorVersion != "" && operatorVersion != inst.Annotations["operatorVersion"] { + logger.V(3).Info("redis inst operatorVersion is not match") + if inst.Annotations == nil { + inst.Annotations = make(map[string]string) + } + inst.Annotations["operatorVersion"] = operatorVersion + return r.updateInstance(ctx, inst, logger) + } + + // ensure redis password secret + if err := r.reconcileSecret(inst, logger); err != nil { + logger.Error(err, "fail to reconcile secret") + return r.updateInstanceStatus(ctx, inst, err, logger) + } + + if inst.Status.UpgradeStatus.CRVersion != bv.Spec.CrVersion { + inst.Status.UpgradeStatus.CRVersion = bv.Spec.CrVersion + _, _ = r.updateInstanceStatus(ctx, inst, err, logger) + } + if ver := inst.Status.UpgradeStatus.CRVersion; ver != "" && ver == inst.Annotations[config.CRUpgradeableVersion] { + delete(inst.Annotations, config.CRUpgradeableVersion) + delete(inst.Annotations, config.CRUpgradeableComponentVersion) + if _, err := r.updateInstance(ctx, inst, logger); err != nil { + logger.Error(err, "fail to update redis instance") + } + } + + // ensure redis inst + switch inst.Spec.Arch { + case core.RedisCluster: + if ptr := inst.Spec.Replicas.Cluster.Slave; ptr == nil || *ptr < 0 { + inst.Spec.Replicas.Cluster.Slave = pointer.Int32(0) + } + + if err := r.reconcileCluster(ctx, inst, bv, logger); err != nil { + logger.Error(err, "fail to reconcile redis cluster") + return r.updateInstanceStatus(ctx, inst, err, logger) + } + case core.RedisSentinel, core.RedisStandalone: + if ptr := inst.Spec.Replicas.Sentinel.Master; ptr == nil || *ptr < 1 { + inst.Spec.Replicas.Sentinel.Master = pointer.Int32(1) + } + if ptr := inst.Spec.Replicas.Sentinel.Slave; ptr == nil || *ptr < 0 { + inst.Spec.Replicas.Sentinel.Slave = pointer.Int32(0) + } + if err := r.reconcileFailover(ctx, inst, bv, logger); err != nil { + logger.Error(err, fmt.Sprintf("fail to reconcile redis %s", inst.Spec.Arch)) + return r.updateInstanceStatus(ctx, inst, err, logger) + } + default: + err = fmt.Errorf("this arch isn't valid, must be cluster, sentinel or standalone") + return ctrl.Result{}, err + } + if err := r.ensurePvcSize(ctx, inst, logger); err != nil { + logger.Error(err, "fail to reconcile pvcs size") + } + if err := r.patchResources(ctx, inst, logger); err != nil { + logger.Error(err, "fail to patch resources") + } + + if !inst.Status.Restored && inst.Status.Phase == rdsv1.RedisPhaseReady { + inst.Status.Restored = true + } + return r.updateInstanceStatus(ctx, inst, err, logger) +} + +func (r *RedisReconciler) patchResources(ctx context.Context, inst *rdsv1.Redis, logger logr.Logger) error { + if inst.Spec.Patches == nil || len(inst.Spec.Patches.Services) == 0 { + return nil + } + for _, psvc := range inst.Spec.Patches.Services { + if psvc.Name == "" { + logger.Error(fmt.Errorf("service name is empty"), "invalid patch service", "service", psvc) + continue + } + + var oldSvc v1.Service + // TODO: only support create now service now + if err := r.Get(ctx, types.NamespacedName{Name: psvc.Name, Namespace: inst.Namespace}, &oldSvc); errors.IsNotFound(err) { + if len(psvc.Spec.Selector) == 0 || len(psvc.Spec.Ports) == 0 { + logger.Error(fmt.Errorf("invalid service"), "please check service ports and selector", "service", psvc) + continue + } + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: psvc.Name, + Namespace: inst.Namespace, + Labels: psvc.Labels, + Annotations: psvc.Annotations, + OwnerReferences: util.BuildOwnerReferences(inst), + }, + Spec: psvc.Spec, + } + if psvc.Labels == nil { + svc.Labels = inst.Status.MatchLabels + } + if err := r.Create(ctx, svc); err != nil { + logger.Error(err, "fail to create patch service", "service", psvc) + continue + } + } else if err != nil { + return err + } else { + // do service merge + isUpdated := false + for _, field := range []struct { + old, new any + }{ + {&oldSvc.Labels, psvc.Labels}, + {&oldSvc.Annotations, psvc.Annotations}, + {&oldSvc.Spec.Selector, psvc.Spec.Selector}, + {&oldSvc.Spec.Type, psvc.Spec.Type}, + {&oldSvc.Spec.IPFamilies, psvc.Spec.IPFamilies}, + {&oldSvc.Spec.IPFamilyPolicy, psvc.Spec.IPFamilyPolicy}, + {&oldSvc.Spec.ExternalIPs, psvc.Spec.ExternalIPs}, + {&oldSvc.Spec.SessionAffinity, psvc.Spec.SessionAffinity}, + {&oldSvc.Spec.SessionAffinityConfig, psvc.Spec.SessionAffinityConfig}, + {&oldSvc.Spec.LoadBalancerSourceRanges, psvc.Spec.LoadBalancerSourceRanges}, + {&oldSvc.Spec.AllocateLoadBalancerNodePorts, psvc.Spec.AllocateLoadBalancerNodePorts}, + {&oldSvc.Spec.ExternalName, psvc.Spec.ExternalName}, + {&oldSvc.Spec.ExternalTrafficPolicy, psvc.Spec.ExternalTrafficPolicy}, + {&oldSvc.Spec.InternalTrafficPolicy, psvc.Spec.InternalTrafficPolicy}, + {&oldSvc.Spec.PublishNotReadyAddresses, psvc.Spec.PublishNotReadyAddresses}, + } { + oldVal := reflect.ValueOf(field.old).Elem() + newVal := reflect.ValueOf(field.new) + + if !newVal.IsZero() && !reflect.DeepEqual(oldVal.Interface(), newVal.Interface()) { + reflect.ValueOf(field.old).Elem().Set(reflect.ValueOf(field.new)) + isUpdated = true + } + } + for _, port := range psvc.Spec.Ports { + oldPort := func() *v1.ServicePort { + for i := range oldSvc.Spec.Ports { + p := &oldSvc.Spec.Ports[i] + if p.Name == port.Name { + return p + } + } + return nil + }() + if oldPort == nil { + if port.Port > 0 { + oldSvc.Spec.Ports = append(oldSvc.Spec.Ports, port) + isUpdated = true + } + } else if !reflect.DeepEqual(*oldPort, port) { + *oldPort = port + isUpdated = true + } + } + if isUpdated { + if err := r.Update(ctx, &oldSvc); err != nil { + logger.Error(err, "fail to update patch service", "service", psvc) + continue + } + } + } + } + return nil +} + +func (r *RedisReconciler) ensurePvcSize(ctx context.Context, inst *rdsv1.Redis, logger logr.Logger) error { + if inst.Spec.Persistent != nil && len(inst.Status.MatchLabels) > 0 { + size := resource.NewQuantity(inst.Spec.Resources.Limits.Memory().Value()*2, resource.BinarySI) + if inst.Spec.PersistentSize != nil { + size = inst.Spec.PersistentSize + } + + switch inst.Spec.Arch { + case core.RedisCluster: + shards := int32(0) + if inst.Spec.Replicas != nil && inst.Spec.Replicas.Cluster != nil && inst.Spec.Replicas.Cluster.Shard != nil { + shards = *inst.Spec.Replicas.Cluster.Shard + } else { + return nil + } + + labels := lo.Assign(inst.Status.MatchLabels) + storageConfigVal := inst.Annotations[rdsv1.RedisClusterPVCSizeAnnotation] + storageConfig := map[int32]string{} + if storageConfigVal != "" { + if err := json.Unmarshal([]byte(storageConfigVal), &storageConfig); err != nil { + logger.Error(err, "fail to unmarshal shard storage config") + return err + } + } + + isUpdated := false + for i := int32(0); i < shards; i++ { + labels["statefulSet"] = clusterbuilder.ClusterStatefulSetName(inst.Name, int(i)) + if maxQuantity, err := redissvc.GetShardMaxPVCQuantity(ctx, r.Client, inst.Namespace, labels); err != nil { + logger.Error(err, "fail to get shard max pvc quantity") + continue + } else { + if storageConfig[i] == "" { + storageConfig[i] = maxQuantity.String() + isUpdated = true + } else if s, err := resource.ParseQuantity(storageConfig[i]); err != nil { + storageConfig[i] = maxQuantity.String() + isUpdated = true + } else if maxQuantity.Cmp(s) > 0 { + storageConfig[i] = maxQuantity.String() + isUpdated = true + } + } + } + if isUpdated { + data, _ := json.Marshal(storageConfig) + inst.Annotations[rdsv1.RedisClusterPVCSizeAnnotation] = string(data) + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst rdsv1.Redis + if err := r.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + return err + } + oldInst.Annotations = inst.Annotations + oldInst.Labels = inst.Labels + oldInst.Spec = inst.Spec + return r.Update(ctx, &oldInst) + }); errors.IsNotFound(err) { + return nil + } else if err != nil { + logger.Error(err, "get redis failed") + } + } + + for i := int32(0); i < shards; i++ { + shardSize := size.DeepCopy() + if s, err := resource.ParseQuantity(storageConfig[i]); err == nil { + shardSize = s + } + labels["statefulSet"] = clusterbuilder.ClusterStatefulSetName(inst.Name, int(i)) + + if err := redissvc.ResizePVCs(ctx, r.Client, inst.Namespace, labels, shardSize); err != nil { + return err + } + } + default: + labels := inst.Status.MatchLabels + if maxQuantity, err := redissvc.GetShardMaxPVCQuantity(ctx, r.Client, inst.Namespace, labels); err != nil { + logger.Error(err, "fail to get shard max pvc quantity") + return err + } else { + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst rdsv1.Redis + if err := r.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + return err + } + if maxQuantity.Cmp(*size) > 0 { + oldInst.Spec.PersistentSize = maxQuantity + size = maxQuantity + if err := r.Update(ctx, &oldInst); err != nil { + logger.Error(err, "fail to update redis inst") + } + } + return nil + }); errors.IsNotFound(err) { + return nil + } else if err != nil { + logger.Error(err, "get redis failed") + } + } + if err := redissvc.ResizePVCs(ctx, r.Client, inst.Namespace, labels, *size); err != nil { + return err + } + } + } + return nil +} + +func (r *RedisReconciler) reconcileFailover(ctx context.Context, inst *rdsv1.Redis, bv *vc.BundleVersion, logger logr.Logger) error { + if inst.Spec.CustomConfig == nil { + inst.Spec.CustomConfig = map[string]string{} + } + + failover := &redisfailover.RedisFailover{} + if err := r.Get(ctx, types.NamespacedName{ + Name: redis.GetRedisFailoverName(inst.Name), + Namespace: inst.Namespace, + }, failover); errors.IsNotFound(err) { + failover, err = redissvc.GenerateRedisFailover(inst, bv) + if err != nil { + return err + } + // record actor versions too keep actions consistent + failover.Annotations[config.CRVersionKey] = bv.Spec.CrVersion + + if err := r.Create(ctx, failover); err != nil { + inst.SetStatusError(err.Error()) + logger.Error(err, "fail to create redis failover inst") + return err + } + inst.Status.MatchLabels = redis.GetRedisSentinelLabels(inst.Name, failover.Name) + return nil + } else if err != nil { + return err + } + + if len(inst.Status.MatchLabels) == 0 { + inst.Status.MatchLabels = redis.GetRedisSentinelLabels(inst.Name, failover.Name) + } + for key := range redissvc.GetRedisConfigsApplyPolicyByVersion(inst.Spec.Version) { + if inst.Spec.CustomConfig[key] != failover.Spec.Redis.CustomConfig[key] { + if inst.Spec.PodAnnotations == nil { + inst.Spec.PodAnnotations = map[string]string{} + } + inst.Spec.PodAnnotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339Nano) + break + } + } + + newFailover, err := redissvc.GenerateRedisFailover(inst, bv) + if err != nil { + logger.Error(err, "fail to generate redis failover inst") + return err + } + newFailover.Annotations[config.CRVersionKey] = bv.Spec.CrVersion + + // TRICK: keep old persistent volume claim, expecially for persistentVolumeClaim name + // we should keep the old pvc name, because the pvc name may not be "redis-data" which created manually + newFailover.Spec.Redis.Storage = failover.Spec.Redis.Storage + + // ensure inst should update + if redissvc.ShouldUpdateFailover(failover, newFailover, logger) { + newFailover.ResourceVersion = failover.ResourceVersion + newFailover.Status = failover.Status + if err := r.updateRedisFailoverInstance(ctx, newFailover); err != nil { + inst.SetStatusError(err.Error()) + logger.Error(err, "fail to update redis failover inst") + return err + } + failover = newFailover + } + + inst.Status.LastShardCount = 1 + inst.Status.LastVersion = inst.Spec.Version + inst.Status.ClusterNodes = failover.Status.Nodes + inst.Status.DetailedStatusRef = failover.Status.DetailedStatusRef + if failover.Status.Phase == redisfailover.Fail { + logger.V(3).Info("redis inst is fail") + inst.SetStatusError(failover.Status.Message) + } else if failover.Status.Phase == redisfailover.Ready { + logger.V(3).Info("redis inst is ready") + inst.SetStatusReady() + } else if failover.Status.Phase == redisfailover.Paused { + logger.V(3).Info("redis inst is paused") + inst.SetStatusPaused() + } else { + logger.V(3).Info("redis inst is unhealthy, waiting redis failover to up", "phase", failover.Status.Phase) + inst.SetStatusUnReady(failover.Status.Message) + } + return nil +} + +func (r *RedisReconciler) reconcileCluster(ctx context.Context, inst *rdsv1.Redis, bv *vc.BundleVersion, logger logr.Logger) error { + cluster := &v1alpha1.DistributedRedisCluster{} + if inst.Spec.PodAnnotations == nil { + inst.Spec.PodAnnotations = make(map[string]string) + } + if inst.Spec.Pause { + inst.Spec.PodAnnotations[redissvc.PauseAnnotationKey] = metav1.NewTime(time.Now()).Format(time.RFC3339) + } + + if err := r.Get(ctx, types.NamespacedName{ + Name: redis.GetRedisClusterName(inst.Name), + Namespace: inst.Namespace, + }, cluster); errors.IsNotFound(err) { + cluster, err = redissvc.GenerateRedisCluster(inst, bv) + if err != nil { + return err + } + // Record actor versions too keep actions consistent + cluster.Annotations[config.CRVersionKey] = bv.Spec.CrVersion + + if err := r.Create(ctx, cluster); err != nil { + inst.SetStatusError(err.Error()) + logger.Error(err, "fail to create redis cluster inst") + return err + } + inst.Status.MatchLabels = redis.GetRedisClusterLabels(inst.Name, cluster.Name) + return nil + } else if err != nil { + return err + } + + if len(inst.Status.MatchLabels) == 0 { + inst.Status.MatchLabels = redis.GetRedisClusterLabels(inst.Name, cluster.Name) + } + for key := range redissvc.GetRedisConfigsApplyPolicyByVersion(inst.Spec.Version) { + if inst.Spec.CustomConfig[key] != cluster.Spec.Config[key] { + if inst.Spec.PodAnnotations == nil { + inst.Spec.PodAnnotations = map[string]string{} + } + inst.Spec.PodAnnotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339Nano) + break + } + } + newCluster, err := redissvc.GenerateRedisCluster(inst, bv) + if err != nil { + return err + } + newCluster.Annotations[config.CRVersionKey] = cluster.Annotations[config.CRVersionKey] + if bv.Spec.CrVersion != cluster.Annotations[config.CRVersionKey] { + // record actor versions too keep actions consistent + newCluster.Annotations[config.CRVersionKey] = bv.Spec.CrVersion + } + // TRICK: keep old persistent volume claim, expecially for persistentVolumeClaim name + // we should keep the old pvc name, because the pvc name may not be "redis-data" which created manually + newCluster.Spec.Storage = cluster.Spec.Storage + + // ensure inst should update + if redissvc.ShouldUpdateCluster(cluster, newCluster, logger) { + newCluster.ResourceVersion = cluster.ResourceVersion + newCluster.Status = cluster.Status + if err := r.updateRedisClusterInstance(ctx, newCluster); err != nil { + inst.SetStatusError(err.Error()) + logger.Error(err, "fail to update redis cluster inst") + return err + } + cluster = newCluster + } + + inst.Status.LastShardCount = cluster.Spec.MasterSize + inst.Status.LastVersion = inst.Spec.Version + inst.Status.ClusterNodes = cluster.Status.Nodes + inst.Status.DetailedStatusRef = cluster.Status.DetailedStatusRef + if redissvc.ClusterIsUp(cluster) { + logger.V(3).Info("redis inst is ready") + inst.SetStatusReady() + } else if cluster.Status.Status == v1alpha1.ClusterStatusPaused { + inst.SetStatusPaused() + } else if cluster.Status.Status == v1alpha1.ClusterStatusRebalancing { + inst.SetStatusRebalancing(cluster.Status.Reason) + } else if cluster.Status.Status == v1alpha1.ClusterStatusKO { + inst.SetStatusError(cluster.Status.Reason) + } else { + logger.V(3).Info("redis inst is unhealthy, waiting redis cluster to up") + inst.SetStatusUnReady(cluster.Status.Reason) + } + return nil +} + +func (r *RedisReconciler) reconcileSecret(inst *rdsv1.Redis, logger logr.Logger) error { + if inst.PasswordIsEmpty() { + inst.Status.PasswordSecretName = "" + return nil + } + + if len(inst.Spec.PasswordSecret) != 0 { //use spec passwd secret + secret := &v1.Secret{} + if err := r.Get(context.TODO(), types.NamespacedName{ + Name: inst.Spec.PasswordSecret, + Namespace: inst.Namespace, + }, secret); err != nil { + logger.Error(err, "fail to get password secret") + if errors.IsNotFound(err) { + inst.Status.PasswordSecretName = "" + } + return err + } + inst.Status.PasswordSecretName = inst.Spec.PasswordSecret + return nil + } + return nil +} + +func (r *RedisReconciler) updateRedisClusterInstance(ctx context.Context, inst *v1alpha1.DistributedRedisCluster) error { + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst v1alpha1.DistributedRedisCluster + if err := r.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + return err + } + inst.ResourceVersion = oldInst.ResourceVersion + return r.Update(ctx, inst) + }); err != nil { + return err + } + return nil +} + +func (r *RedisReconciler) updateRedisFailoverInstance(ctx context.Context, inst *redisfailover.RedisFailover) error { + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst redisfailover.RedisFailover + if err := r.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + return err + } + inst.ResourceVersion = oldInst.ResourceVersion + return r.Update(ctx, inst) + }); err != nil { + return err + } + return nil +} + +func (r *RedisReconciler) updateInstanceStatus(ctx context.Context, inst *rdsv1.Redis, err error, logger logr.Logger) (ctrl.Result, error) { + logger.V(3).Info("updating inst state") + if err != nil { + inst.SetStatusError(err.Error()) + } else { + inst.RecoverStatusError() + } + + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst rdsv1.Redis + if err := r.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + return err + } + if reflect.DeepEqual(oldInst.Status, inst.Status) { + return nil + } + inst.ResourceVersion = oldInst.ResourceVersion + return r.Status().Update(ctx, inst) + }); errors.IsNotFound(err) { + return ctrl.Result{}, nil + } else { + return ctrl.Result{RequeueAfter: requeueSecond}, err + } +} + +func (r *RedisReconciler) updateInstance(ctx context.Context, inst *rdsv1.Redis, logger logr.Logger) (ctrl.Result, error) { + logger.V(3).Info("updating instance") + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst rdsv1.Redis + if err := r.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + return err + } + inst.ResourceVersion = oldInst.ResourceVersion + return r.Update(ctx, inst) + }); errors.IsNotFound(err) { + return ctrl.Result{}, nil + } else { + return ctrl.Result{RequeueAfter: requeueSecond}, err + } +} + +func (r *RedisReconciler) processFinalizer(inst *rdsv1.Redis) error { + for _, v := range inst.GetFinalizers() { + if v == pvcFinalizer { + // delete pvcs + if inst.Status.MatchLabels == nil { + return fmt.Errorf("can't delete inst pvcs, status.matchLabels is empty") + } + err := r.DeleteAllOf(context.TODO(), &v1.PersistentVolumeClaim{}, client.InNamespace(inst.Namespace), + client.MatchingLabels(inst.Status.MatchLabels)) + if err != nil { + return err + } + controllerutil.RemoveFinalizer(inst, v) + err = r.Update(context.TODO(), inst) + if err != nil { + return err + } + } + } + return nil +} + +func (r *RedisReconciler) createRedisByManagerUICreatedWithCluster(ctx context.Context, nsn types.NamespacedName) error { + nsn.Name = strings.ReplaceAll(nsn.Name, createRedisCluster, "") + cluster := &v1alpha1.DistributedRedisCluster{} + err := r.Get(ctx, nsn, cluster) + if err != nil { + return err + } + + // check if cluster disabled rds management for special cases + // eg. devops doesn't need rds + if val := cluster.Annotations[DisableRdsManagementAnnotationKey]; strings.ToLower(val) == "true" { + return nil + } + + var secret *v1.Secret + if cluster.Spec.PasswordSecret != nil { + secret = &v1.Secret{} + err = r.Get(ctx, types.NamespacedName{ + Name: cluster.Spec.PasswordSecret.Name, + Namespace: cluster.Namespace, + }, secret) + if err != nil { + return err + } + + } + inst := redissvc.GenerateClusterRedisByManagerUI(cluster, r.Scheme, secret) + if inst.Spec.Version == "" { + return fmt.Errorf("version is not valid, available version: 4.0, 5.0, 6.0") + } + //创建资源 + if err = r.Create(ctx, inst); err != nil { + if !errors.IsAlreadyExists(err) { + return err + } + } + if err = controllerutil.SetControllerReference(inst, cluster, r.Scheme); err != nil { + return err + } + return r.Update(ctx, cluster) +} + +func (r *RedisReconciler) createRedisByManagerUICreatedWithSentinel(ctx context.Context, nsn types.NamespacedName) error { + nsn.Name = strings.ReplaceAll(nsn.Name, createRedisSentinel, "") + failover := &redisfailover.RedisFailover{} + err := r.Get(ctx, nsn, failover) + if err != nil { + return err + } + + // check if failover disabled rds management for special cases + // eg. devops doesn't need rds + if val := failover.Annotations[DisableRdsManagementAnnotationKey]; strings.ToLower(val) == "true" { + return nil + } + + sts := &appsv1.StatefulSet{} + err = r.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("rfr-%s", nsn.Name), Namespace: nsn.Namespace}, sts) + if err != nil { + return err + } + var secret *v1.Secret + if failover.Spec.Auth.SecretPath != "" { + secret = &v1.Secret{} + err = r.Get(ctx, types.NamespacedName{ + Name: failover.Spec.Auth.SecretPath, + Namespace: failover.Namespace, + }, secret) + if err != nil { + return err + } + } + inst := redissvc.GenerateFailoverRedisByManagerUI(failover, sts, secret) + if inst.Spec.Version == "" { + return fmt.Errorf("version is not valid, available version: 4.0, 5.0, 6.0") + } + + if err = r.Create(ctx, inst); err != nil { + if !errors.IsAlreadyExists(err) { + return err + } + } + if err = controllerutil.SetControllerReference(inst, failover, r.Scheme); err != nil { + return err + } + return r.Update(ctx, failover) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RedisReconciler) SetupWithManager(mgr ctrl.Manager) error { + + return ctrl.NewControllerManagedBy(mgr). + For(&rdsv1.Redis{}). + WithOptions(controller.Options{MaxConcurrentReconciles: 4}). + Owns(&v1alpha1.DistributedRedisCluster{}). + Owns(&redisfailover.RedisFailover{}). + Watches(&redisfailover.RedisFailover{}, handler.Funcs{ + CreateFunc: func(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { + for _, v := range e.Object.GetOwnerReferences() { + if v.Kind == "Redis" { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) + return + } + } + + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName() + createRedisSentinel, + Namespace: e.Object.GetNamespace(), + }}) + }}). + Watches(&v1alpha1.DistributedRedisCluster{}, handler.Funcs{ + CreateFunc: func(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { + for _, v := range e.Object.GetOwnerReferences() { + if v.Kind == "Redis" { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) + return + } + } + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName() + createRedisCluster, + Namespace: e.Object.GetNamespace(), + }}) + }}). + Complete(r) +} diff --git a/internal/controller/middleware/redisuser/handler.go b/internal/controller/middleware/redisuser/handler.go new file mode 100644 index 0000000..0bd4575 --- /dev/null +++ b/internal/controller/middleware/redisuser/handler.go @@ -0,0 +1,269 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package redisuser + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/types/user" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/retry" + + "github.com/alauda/redis-operator/api/core" + ruv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + "github.com/alauda/redis-operator/internal/redis/cluster" + "github.com/alauda/redis-operator/internal/redis/failover" + "github.com/alauda/redis-operator/pkg/kubernetes" +) + +type RedisUserHandler struct { + k8sClient kubernetes.ClientSet + eventRecorder record.EventRecorder + logger logr.Logger +} + +func NewRedisUserHandler(k8sservice kubernetes.ClientSet, eventRecorder record.EventRecorder, logger logr.Logger) *RedisUserHandler { + return &RedisUserHandler{ + k8sClient: k8sservice, + eventRecorder: eventRecorder, + logger: logger.WithName("RedisUserHandler"), + } +} + +func (r *RedisUserHandler) Delete(ctx context.Context, inst ruv1.RedisUser, logger logr.Logger) error { + logger.V(3).Info("redis user delete", "redis user name", inst.Name, "type", inst.Spec.Arch) + if inst.Spec.Username == user.DefaultUserName || inst.Spec.Username == user.DefaultOperatorUserName { + return nil + } + + name := clusterbuilder.GenerateClusterACLConfigMapName(inst.Spec.RedisName) + if inst.Spec.Arch == core.RedisSentinel { + name = failoverbuilder.GenerateFailoverACLConfigMapName(inst.Spec.RedisName) + } + if configMap, err := r.k8sClient.GetConfigMap(ctx, inst.Namespace, name); err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "delete user from configmap failed") + return err + } + } else if _, ok := configMap.Data[inst.Spec.Username]; ok { + delete(configMap.Data, inst.Spec.Username) + if err := r.k8sClient.UpdateConfigMap(ctx, inst.Namespace, configMap); err != nil { + logger.Error(err, "delete user from configmap failed", "configmap", configMap.Name) + return err + } + } + + switch inst.Spec.Arch { + case core.RedisCluster: + logger.V(3).Info("cluster", "redis name", inst.Spec.RedisName) + rc, err := r.k8sClient.GetDistributedRedisCluster(ctx, inst.Namespace, inst.Spec.RedisName) + if errors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + rcm, err := cluster.NewRedisCluster(ctx, r.k8sClient, r.eventRecorder, rc, logger) + if err != nil { + return err + } + if !rcm.IsReady() { + logger.V(3).Info("redis instance is not ready", "redis name", inst.Spec.RedisName) + return fmt.Errorf("redis instance is not ready") + } + + for _, node := range rcm.Nodes() { + err := node.Setup(ctx, []interface{}{"ACL", "DELUSER", inst.Spec.Username}) + if err != nil { + logger.Error(err, "acl del user failed", "node", node.GetName()) + return err + } + logger.V(3).Info("acl del user success", "node", node.GetName()) + } + case core.RedisSentinel, core.RedisStandalone: + logger.V(3).Info("sentinel", "redis name", inst.Spec.RedisName) + rf, err := r.k8sClient.GetRedisFailover(ctx, inst.Namespace, inst.Spec.RedisName) + if errors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + rfm, err := failover.NewRedisFailover(ctx, r.k8sClient, r.eventRecorder, rf, logger) + if err != nil { + return err + } + if !rfm.IsReady() { + logger.V(3).Info("redis instance is not ready", "redis name", inst.Spec.RedisName) + return fmt.Errorf("redis instance is not ready") + } + for _, node := range rfm.Nodes() { + err := node.Setup(ctx, []interface{}{"ACL", "DELUSER", inst.Spec.Username}) + if err != nil { + logger.Error(err, "acl del user failed", "node", node.GetName()) + return err + } + logger.V(3).Info("acl del user success", "node", node.GetName()) + } + } + return nil +} + +func (r *RedisUserHandler) Do(ctx context.Context, inst ruv1.RedisUser, logger logr.Logger) error { + if inst.Annotations == nil { + inst.Annotations = map[string]string{} + } + + passwords := []string{} + // operators account skip + if inst.Spec.AccountType == ruv1.System { + return nil + } + + userPassword := &user.Password{} + for _, secretName := range inst.Spec.PasswordSecrets { + secret, err := r.k8sClient.GetSecret(ctx, inst.Namespace, secretName) + if err != nil { + return err + } + if secret.GetLabels() == nil { + secret.SetLabels(map[string]string{}) + } + + if secret.Labels[builder.InstanceNameLabel] != inst.Spec.RedisName || + len(secret.GetOwnerReferences()) == 0 || secret.OwnerReferences[0].UID != inst.GetUID() { + + secret.Labels[builder.ManagedByLabel] = "redis-operator" + secret.Labels[builder.InstanceNameLabel] = inst.Spec.RedisName + secret.OwnerReferences = util.BuildOwnerReferences(&inst) + if err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.k8sClient.UpdateSecret(ctx, inst.Namespace, secret) + }); err != nil { + logger.Error(err, "update secret owner failed", "secret", secret.Name) + return err + } + } + passwords = append(passwords, string(secret.Data["password"])) + userPassword = &user.Password{ + SecretName: secretName, + } + } + + logger.V(3).Info("redis user do", "redis user name", inst.Name, "type", inst.Spec.Arch) + switch inst.Spec.Arch { + case core.RedisCluster: + logger.V(3).Info("cluster", "redis name", inst.Spec.RedisName) + rc, err := r.k8sClient.GetDistributedRedisCluster(ctx, inst.Namespace, inst.Spec.RedisName) + if err != nil { + return err + } + + rcm, err := cluster.NewRedisCluster(ctx, r.k8sClient, r.eventRecorder, rc, logger) + if err != nil { + return err + } + if !rcm.IsReady() { + logger.V(3).Info("redis instance is not ready", "redis name", inst.Spec.RedisName) + return fmt.Errorf("redis instance is not ready") + } + + aclRules := inst.Spec.AclRules + if rcm.Version().IsACLSupported() { + rule, err := user.NewRule(inst.Spec.AclRules) + if err != nil { + logger.V(3).Info("rule parse failed", "rule", inst.Spec.AclRules) + return err + } + rule = user.PatchRedisClusterClientRequiredRules(rule) + aclRules = rule.Encode() + } + userObj, err := user.NewUserFromRedisUser(inst.Spec.Username, aclRules, userPassword) + if err != nil { + return err + } + info, err := json.Marshal(userObj) + if err != nil { + return err + } + + configmap, err := r.k8sClient.GetConfigMap(ctx, inst.Namespace, clusterbuilder.GenerateClusterACLConfigMapName(inst.Spec.RedisName)) + if err != nil { + return err + } + configmap.Data[inst.Spec.Username] = string(info) + for _, node := range rcm.Nodes() { + _, err := node.SetACLUser(ctx, inst.Spec.Username, passwords, aclRules) + if err != nil { + logger.Error(err, "acl set user failed", "node", node.GetName()) + return err + } + logger.V(3).Info("acl set user success", "node", node.GetName()) + } + if err := r.k8sClient.UpdateConfigMap(ctx, inst.Namespace, configmap); err != nil { + logger.Error(err, "update configmap failed", "configmap", configmap.Name) + return err + } + case core.RedisSentinel, core.RedisStandalone: + logger.V(3).Info("sentinel", "redis name", inst.Spec.RedisName) + rf, err := r.k8sClient.GetRedisFailover(ctx, inst.Namespace, inst.Spec.RedisName) + if err != nil { + return err + } + rfm, err := failover.NewRedisFailover(ctx, r.k8sClient, r.eventRecorder, rf, logger) + if err != nil { + return err + } + if !rfm.IsReady() { + logger.V(3).Info("redis instance is not ready", "redis name", inst.Spec.RedisName) + return fmt.Errorf("redis instance is not ready") + } + configmap, err := r.k8sClient.GetConfigMap(ctx, inst.Namespace, failoverbuilder.GenerateFailoverACLConfigMapName(inst.Spec.RedisName)) + if err != nil { + return err + } + userObj, err := user.NewUserFromRedisUser(inst.Spec.Username, inst.Spec.AclRules, userPassword) + if err != nil { + return err + } + info, err := json.Marshal(userObj) + if err != nil { + return err + } + configmap.Data[inst.Spec.Username] = string(info) + for _, node := range rfm.Nodes() { + _, err := node.SetACLUser(ctx, inst.Spec.Username, passwords, inst.Spec.AclRules) + if err != nil { + logger.Error(err, "acl set user failed", "node", node.GetName()) + return err + } + logger.V(3).Info("acl set user success", "node", node.GetName()) + } + if err := r.k8sClient.UpdateConfigMap(ctx, inst.Namespace, configmap); err != nil { + logger.Error(err, "update configmap failed", "configmap", configmap.Name) + return err + } + } + return nil +} diff --git a/internal/controller/redis/redisuser_controller.go b/internal/controller/middleware/redisuser_controller.go similarity index 53% rename from internal/controller/redis/redisuser_controller.go rename to internal/controller/middleware/redisuser_controller.go index 59cbb49..411b7c7 100644 --- a/internal/controller/redis/redisuser_controller.go +++ b/internal/controller/middleware/redisuser_controller.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package redis +package middleware import ( "context" @@ -22,18 +22,25 @@ import ( "strings" "time" - redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/internal/controller/redis/redisuser" + redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/controller/middleware/redisuser" "github.com/alauda/redis-operator/pkg/kubernetes" + security "github.com/alauda/redis-operator/pkg/security/password" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -43,9 +50,9 @@ type RedisUserReconciler struct { client.Client K8sClient kubernetes.ClientSet Scheme *runtime.Scheme - Logger logr.Logger Record record.EventRecorder Handler redisuser.RedisUserHandler + Logger logr.Logger } //+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisusers,verbs=get;list;watch;create;update;patch;delete @@ -53,93 +60,140 @@ type RedisUserReconciler struct { //+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisusers/finalizers,verbs=update func (r *RedisUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Logger.Info("redis user reconcile", "redis user name", req.Name) + logger := log.FromContext(context.TODO()).WithName("RedisUser").WithValues("namespace", req.Namespace, "name", req.Name) + instance := redismiddlewarealaudaiov1.RedisUser{} err := r.Client.Get(ctx, req.NamespacedName, &instance) if err != nil { - r.Logger.Error(err, "get redis user failed") + logger.Error(err, "get redis user failed") if errors.IsNotFound(err) { return reconcile.Result{}, nil } return reconcile.Result{}, err } + isMarkedToBeDeleted := instance.GetDeletionTimestamp() != nil if isMarkedToBeDeleted { - r.Logger.Info("finalizeRedisUser", "Namespace", instance.Namespace, "Instance", instance.Name) - if err := r.Handler.Delete(ctx, instance); err != nil { + logger.Info("finalizeRedisUser", "Namespace", instance.Namespace, "Instance", instance.Name) + if err := r.Handler.Delete(ctx, instance, logger); err != nil { if instance.Status.Message != err.Error() { instance.Status.Phase = redismiddlewarealaudaiov1.Fail - instance.Status.Message = "Delete Fail:" + err.Error() + instance.Status.Message = "clean user failed with error " + err.Error() if _err := r.Client.Status().Update(ctx, &instance); _err != nil { - r.Logger.Error(_err, "update user status failed", "instance", req.NamespacedName) + logger.Error(_err, "update user status failed", "instance", req.NamespacedName) return ctrl.Result{}, err } } return ctrl.Result{RequeueAfter: time.Second * 30}, err } else { - r.Logger.Info("RemoveFinalizer", "Namespace", instance.Namespace, "Instance", instance.Name) + logger.Info("RemoveFinalizer", "Namespace", instance.Namespace, "Instance", instance.Name) controllerutil.RemoveFinalizer(&instance, redismiddlewarealaudaiov1.RedisUserFinalizer) - err := r.Update(ctx, &instance) - if err != nil { + if err := r.Update(ctx, &instance); err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, nil } - if err := r.Handler.Do(ctx, instance); err != nil { + + // verify redis user password + for _, name := range instance.Spec.PasswordSecrets { + if name == "" { + continue + } + secret := &v1.Secret{} + if err := r.Get(context.Background(), types.NamespacedName{ + Namespace: instance.Namespace, + Name: name, + }, secret); err != nil { + logger.Info("get secret failed", "secret name", name) + instance.Status.Message = err.Error() + instance.Status.Phase = redismiddlewarealaudaiov1.Fail + return ctrl.Result{}, r.Client.Status().Update(ctx, &instance) + } else if err := security.PasswordValidate(string(secret.Data["password"]), 8, 32); err != nil { + if instance.Spec.AccountType != redismiddlewarealaudaiov1.System { + instance.Status.Message = err.Error() + instance.Status.Phase = redismiddlewarealaudaiov1.Fail + return ctrl.Result{RequeueAfter: time.Minute}, r.Client.Status().Update(ctx, &instance) + } + } + } + + if err := r.Handler.Do(ctx, instance, logger); err != nil { if strings.Contains(err.Error(), "redis instance is not ready") || strings.Contains(err.Error(), "node not ready") || + strings.Contains(err.Error(), "user not operator") || strings.Contains(err.Error(), "ERR unknown command `ACL`") { - r.Logger.Info("redis instance is not ready", "redis name", instance.Spec.RedisName) + logger.V(3).Info("redis instance is not ready", "redis name", instance.Spec.RedisName) instance.Status.Message = err.Error() instance.Status.Phase = redismiddlewarealaudaiov1.Pending instance.Status.LastUpdatedSuccess = time.Now().Format(time.RFC3339) - _err := r.Client.Status().Update(ctx, &instance) - if _err != nil { - r.Logger.Error(_err, "update redis user status failed") + if err := r.updateRedisUserStatus(ctx, &instance); err != nil { + logger.Error(err, "update redis user status to Pending failed") } - return ctrl.Result{RequeueAfter: time.Second * 30}, nil + return ctrl.Result{RequeueAfter: time.Second * 15}, nil } + instance.Status.Message = err.Error() instance.Status.Phase = redismiddlewarealaudaiov1.Fail instance.Status.LastUpdatedSuccess = time.Now().Format(time.RFC3339) - r.Logger.Error(err, "redis user reconcile failed") - _err := r.Client.Status().Update(ctx, &instance) - if _err != nil { - r.Logger.Error(_err, "update redis user status failed") + logger.Error(err, "redis user reconcile failed") + if err := r.updateRedisUserStatus(ctx, &instance); err != nil { + logger.Error(err, "update redis user status to Fail failed") } - return reconcile.Result{}, err + return reconcile.Result{RequeueAfter: time.Second * 10}, nil } instance.Status.Phase = redismiddlewarealaudaiov1.Success instance.Status.Message = "" instance.Status.LastUpdatedSuccess = time.Now().Format(time.RFC3339) - r.Logger.V(3).Info("redis user reconcile success") - err = r.Client.Status().Update(ctx, &instance) - if err != nil { - r.Logger.Error(err, "update redis user status failed") + logger.V(3).Info("redis user reconcile success") + if err := r.updateRedisUserStatus(ctx, &instance); err != nil { + logger.Error(err, "update redis user status to Success failed") + return reconcile.Result{RequeueAfter: time.Second * 10}, err } if !controllerutil.ContainsFinalizer(&instance, redismiddlewarealaudaiov1.RedisUserFinalizer) { controllerutil.AddFinalizer(&instance, redismiddlewarealaudaiov1.RedisUserFinalizer) - err := r.Update(ctx, &instance) - if err != nil { - r.Logger.Error(err, "update redis finalizer user failed") + if err := r.updateRedisUser(ctx, &instance); err != nil { + logger.Error(err, "update redis finalizer user failed") return ctrl.Result{}, err } } return ctrl.Result{}, nil } +func (r *RedisUserReconciler) updateRedisUserStatus(ctx context.Context, inst *redismiddlewarealaudaiov1.RedisUser) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldUser redismiddlewarealaudaiov1.RedisUser + if err := r.Get(ctx, types.NamespacedName{Namespace: inst.Namespace, Name: inst.Name}, &oldUser); err != nil { + return err + } + inst.ResourceVersion = oldUser.ResourceVersion + return r.Status().Update(ctx, inst) + }) +} + +func (r *RedisUserReconciler) updateRedisUser(ctx context.Context, inst *redismiddlewarealaudaiov1.RedisUser) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldUser redismiddlewarealaudaiov1.RedisUser + if err := r.Get(ctx, types.NamespacedName{Namespace: inst.Namespace, Name: inst.Name}, &oldUser); err != nil { + return err + } + inst.ResourceVersion = oldUser.ResourceVersion + return r.Update(ctx, inst) + }) +} + func (r *RedisUserReconciler) SetupEventRecord(mgr ctrl.Manager) { r.Record = mgr.GetEventRecorderFor("redis-user-operator") } // SetupWithManager sets up the controller with the Manager. func (r *RedisUserReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.Handler = *redisuser.NewRedisUserHandler(r.K8sClient, r.Logger) + r.Handler = *redisuser.NewRedisUserHandler(r.K8sClient, r.Record, r.Logger) return ctrl.NewControllerManagedBy(mgr). For(&redismiddlewarealaudaiov1.RedisUser{}). Owns(&corev1.Secret{}). WithEventFilter(CustomGenerationChangedPredicate(r.Logger)). + WithOptions(controller.Options{MaxConcurrentReconciles: 4}). Complete(r) } @@ -149,7 +203,6 @@ func (r *RedisUserReconciler) SetupWithManager(mgr ctrl.Manager) error { func CustomGenerationChangedPredicate(logger logr.Logger) predicate.Predicate { return predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { - //logger.Info("UpdateFunc", "old", e.ObjectOld, "new", e.ObjectNew, "generation", e.ObjectNew.GetGeneration()) if e.ObjectOld == nil { return false } diff --git a/internal/controller/redis/suite_test.go b/internal/controller/middleware/suite_test.go similarity index 88% rename from internal/controller/redis/suite_test.go rename to internal/controller/middleware/suite_test.go index 9161e82..3bf02af 100644 --- a/internal/controller/redis/suite_test.go +++ b/internal/controller/middleware/suite_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package redis +package middleware import ( "path/filepath" @@ -30,8 +30,8 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - middlewarev1 "github.com/alauda/redis-operator/api/redis/v1" - rdsv1 "github.com/alauda/redis-operator/api/redis/v1" + middlewarev1 "github.com/alauda/redis-operator/api/middleware/v1" + rdsv1 "github.com/alauda/redis-operator/api/middleware/v1" //+kubebuilder:scaffold:imports ) @@ -53,7 +53,7 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } diff --git a/internal/controller/redis/redisbackup/handler.go b/internal/controller/redis/redisbackup/handler.go deleted file mode 100644 index d938d85..0000000 --- a/internal/controller/redis/redisbackup/handler.go +++ /dev/null @@ -1,190 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redisbackup - -import ( - "context" - "errors" - "net/url" - "reflect" - - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/internal/controller/redis/redisbackup/service" - "github.com/alauda/redis-operator/pkg/config" - k8s "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - rfLabelManagedByKey = "app.kubernetes.io/managed-by" - rfLabelNameKey = "redisbackups.redis.middleware.alauda.io/name" - labelInstanceKey = "redisbackups.redis.middleware.alauda.io/instanceName" - - operatorName = "redis-backup" -) - -var ( - defaultLabels = map[string]string{ - rfLabelManagedByKey: operatorName, - } -) - -// RedisBackupHandler is the Redis Backup handler. This handler will create the required -// resources that a RF needs. -type RedisBackupHandler struct { - backupService service.RedisBackupClient - k8sservices k8s.ClientSet - logger logr.Logger -} - -// NewRedisBackupHandler returns a new RF handler -func NewRedisBackupHandler(k8sservice k8s.ClientSet, backupService service.RedisBackupClient, logger logr.Logger) *RedisBackupHandler { - return &RedisBackupHandler{ - k8sservices: k8sservice, - backupService: backupService, - logger: logger, - } -} - -func (r *RedisBackupHandler) Ensure(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - or := createOwnerReferences(backup) - // Create the labels every object derived from this need to have. - labels := getLabels(backup) - - if backup.Spec.Source.RedisName != "" && backup.Spec.Source.SourceType == redisbackupv1.Cluster { - if err := r.backupService.EnsureInfoAnnotationsAndLabelsForCluster(ctx, backup); err != nil { - return err - } - } else { - if err := r.backupService.EnsureInfoAnnotationsAndLabels(ctx, backup); err != nil { - return err - } - } - if err := r.backupService.EnsureRoleReady(ctx, backup); err != nil { - return err - } - if err := r.backupService.EnsureStorageReady(ctx, backup, labels, or); err != nil { - return err - } - if err := r.backupService.EnsureBackupJobCreated(ctx, backup, labels, or); err != nil { - return err - } - if err := r.backupService.EnsureBackupCompleted(ctx, backup); err != nil { - return err - } - if err := r.backupService.UpdateBackupStatus(ctx, backup); err != nil { - return err - } - return nil -} - -// getLabels merges the labels (dynamic and operator static ones). -func getLabels(rf *redisbackupv1.RedisBackup) map[string]string { - dynLabels := map[string]string{ - rfLabelNameKey: rf.Name, - labelInstanceKey: rf.Spec.Source.RedisFailoverName, - } - return util.MergeMap(defaultLabels, dynLabels) -} - -func createOwnerReferences(rf *redisbackupv1.RedisBackup) []metav1.OwnerReference { - t := reflect.TypeOf(&redisbackupv1.RedisBackup{}).Elem() - - ownerRef := metav1.NewControllerRef(rf, redisbackupv1.GroupVersion.WithKind(t.Name())) - ownerRef.BlockOwnerDeletion = nil - ownerRef.Controller = nil - return []metav1.OwnerReference{*ownerRef} -} - -func (r *RedisBackupHandler) DeleteS3(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - if backup.Spec.Target.S3Option.S3Secret == "" { - return nil - } - secret, err := r.k8sservices.GetSecret(ctx, backup.Namespace, backup.Spec.Target.S3Option.S3Secret) - if err != nil { - return err - } - token := "" - if v, ok := secret.Data[config.S3_TOKEN]; ok { - token = string(v) - } - secure := false - s3_url := string(secret.Data[config.S3_ENDPOINTURL]) - endpoint, err := url.Parse(s3_url) - if err != nil { - return err - } - if endpoint.Scheme == "https" { - secure = true - } - - s3Client, err := minio.New(endpoint.Host, - &minio.Options{ - Creds: credentials.NewStaticV4(string(secret.Data[config.S3_ACCESS_KEY_ID]), string(secret.Data[config.S3_SECRET_ACCESS_KEY]), token), - Region: string(secret.Data[config.S3_REGION]), - Secure: secure, - }) - if err != nil { - return err - } - if exists, err := s3Client.BucketExists(ctx, backup.Spec.Target.S3Option.Bucket); err != nil { - return err - } else if !exists { - return errors.New("S3 Bucket no exists") - } - var err_list []error - objectsCh := make(chan minio.ObjectInfo) - - go func() { - defer close(objectsCh) - opts := minio.ListObjectsOptions{Prefix: backup.Spec.Target.S3Option.Dir, Recursive: true} - for object := range s3Client.ListObjects(ctx, backup.Spec.Target.S3Option.Bucket, opts) { - if object.Err != nil { - r.logger.Info("ListObjects", "Err", object.Err.Error()) - err_list = append(err_list, object.Err) - } - objectsCh <- object - } - }() - - errorCh := s3Client.RemoveObjects(ctx, backup.Spec.Target.S3Option.Bucket, objectsCh, minio.RemoveObjectsOptions{}) - for e := range errorCh { - if e.Err != nil { - return err - } - } - if len(err_list) != 0 { - err_text := "ListObjects Err:" - for _, v := range err_list { - err_text += v.Error() - } - return errors.New(err_text) - } - if len(err_list) != 0 { - err_text := "ListObjects Err:" - for _, v := range err_list { - err_text += v.Error() - } - return errors.New(err_text) - } - return nil - -} diff --git a/internal/controller/redis/redisbackup/service/client.go b/internal/controller/redis/redisbackup/service/client.go deleted file mode 100644 index ae8cc72..0000000 --- a/internal/controller/redis/redisbackup/service/client.go +++ /dev/null @@ -1,333 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package service - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/config" - k8s "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/util" - - "github.com/go-logr/logr" - batchv1 "k8s.io/api/batch/v1" - v1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// RedisBackupClient has the minimumm methods that a Redis backup controller needs to satisfy -// in order to talk with K8s -type RedisBackupClient interface { - EnsureStorageReady(ctx context.Context, backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error - EnsureBackupJobCreated(ctx context.Context, backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error - EnsureBackupCompleted(ctx context.Context, backup *redisbackupv1.RedisBackup) error - UpdateBackup(ctx context.Context, backup *redisbackupv1.RedisBackup) error - UpdateBackupStatus(ctx context.Context, backup *redisbackupv1.RedisBackup) error - EnsureRoleReady(ctx context.Context, backup *redisbackupv1.RedisBackup) error - EnsureInfoAnnotationsAndLabels(ctx context.Context, backup *redisbackupv1.RedisBackup) error - EnsureInfoAnnotationsAndLabelsForCluster(ctx context.Context, backup *redisbackupv1.RedisBackup) error -} - -// RedisBackupKubeClient implements the required methods to talk with kubernetes -type RedisBackupKubeClient struct { - K8SService k8s.ClientSet - logger logr.Logger -} - -// NewRedisBackupKubeClient creates a new RedisFailoverKubeClient -func NewRedisBackupKubeClient(k8sService k8s.ClientSet, logger logr.Logger) *RedisBackupKubeClient { - return &RedisBackupKubeClient{ - K8SService: k8sService, - logger: logger, - } -} - -func (r *RedisBackupKubeClient) EnsureInfoAnnotationsAndLabelsForCluster(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - instance, err := r.K8SService.GetDistributedRedisCluster(ctx, backup.Namespace, backup.Spec.Source.RedisName) - if err != nil { - return err - } - if instance.Status.Reason != "OK" && instance.Status.Status != v1alpha1.ClusterStatusOK { - return fmt.Errorf("sentinel cluster :%s, is not Ready", instance.Name) - } - if backup.Annotations == nil { - backup.Annotations = map[string]string{} - } - res, _ := json.Marshal(instance.Spec.Resources) - backup.Annotations["sourceResources"] = string(res) - backup.Annotations["sourceClusterReplicasShard"] = strconv.Itoa(int(instance.Spec.MasterSize)) - backup.Annotations["sourceClusterReplicasSlave"] = strconv.Itoa(int(instance.Spec.ClusterReplicas)) - if backup.Labels == nil { - backup.Labels = map[string]string{} - } - backup.Labels["redis.kun/name"] = backup.Spec.Source.RedisName - return nil -} - -func (r *RedisBackupKubeClient) EnsureInfoAnnotationsAndLabels(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - instance, err := r.K8SService.GetRedisFailover(ctx, backup.Namespace, backup.Spec.Source.RedisFailoverName) - if err != nil { - if errors.IsNotFound(err) { - instance, err = r.K8SService.GetRedisFailover(ctx, backup.Namespace, backup.Spec.Source.RedisName) - if err != nil { - return err - } - } else { - return err - } - } - if instance.Spec.Redis.Resources.Limits.Memory().Cmp(backup.Spec.Storage) > 0 { - backup.Spec.Storage = *instance.Spec.Redis.Resources.Limits.Memory() - } - - if err := AddBackupAnnotationsAndLabels(instance, backup); err != nil { - return err - } - return r.UpdateBackup(ctx, backup) -} - -func AddBackupAnnotationsAndLabels(instance *databasesv1.RedisFailover, backup *redisbackupv1.RedisBackup) error { - if instance.Status.Phase != "Ready" { - return fmt.Errorf("sentinel cluster %s is not ready", instance.Name) - } - - if backup.Annotations == nil { - backup.Annotations = make(map[string]string) - } - res, err := json.Marshal(instance.Spec.Redis.Resources) - if err != nil { - return err - } - backup.Annotations["sourceResources"] = string(res) - backup.Annotations["sourceSentinelReplicasMaster"] = "1" - if instance.Spec.Sentinel.Replicas == 0 { - instance.Spec.Sentinel.Replicas = databasesv1.DefaultSentinelNumber - } - backup.Annotations["sourceSentinelReplicasSlave"] = strconv.Itoa(int(instance.Spec.Sentinel.Replicas) - 1) - backup.Annotations["sourceClusterVersion"] = config.GetRedisVersion(instance.Spec.Redis.Image) - if backup.Labels == nil { - backup.Labels = make(map[string]string) - } - if backup.Spec.Source.RedisName != "" { - backup.Labels["redisfailovers.databases.spotahome.com/name"] = backup.Spec.Source.RedisName - } else { - backup.Labels["redisfailovers.databases.spotahome.com/name"] = backup.Spec.Source.RedisFailoverName - } - - return nil -} - -func (r *RedisBackupKubeClient) EnsureStorageReady(ctx context.Context, backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error { - if backup.Status.Destination != "" { - return nil - } - if backup.Spec.Source.StorageClassName == "" { - return nil - } - pvc := generatorPVC(backup, labels, ownerRefs) - old_pvcs, err := r.K8SService.ListPvcByLabel(ctx, pvc.Namespace, labels) - if err != nil { - return err - } - if old_pvcs == nil || len(old_pvcs.Items) == 0 { - err := r.K8SService.CreatePVC(ctx, backup.Namespace, pvc) - if err != nil { - return err - } - backup.Status.Destination = fmt.Sprintf("%s/%s", "pvc", pvc.Name) - } else if len(old_pvcs.Items) > 0 { - backup.Status.Destination = fmt.Sprintf("%s/%s", "pvc", old_pvcs.Items[0].Name) - } - - return nil -} - -func (r *RedisBackupKubeClient) EnsureBackupJobCreated(ctx context.Context, backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error { - if backup.Status.JobName != "" { - return nil - } - job := &batchv1.Job{} - if backup.Spec.Target.S3Option.S3Secret != "" { - rf, err := r.K8SService.GetRedisFailover(ctx, backup.Namespace, backup.Spec.Source.RedisName) - if err := rf.Validate(); err != nil { - return err - } - if err != nil { - return err - } - if backup.Spec.Source.RedisName != "" { - config_map := generateBackupConfigMap(backup, labels, ownerRefs, rf) - if err = r.K8SService.CreateIfNotExistsConfigMap(ctx, rf.Namespace, config_map); err != nil { - return err - } - job = generateBackupJobForS3(backup, labels, ownerRefs, rf) - } - - } else if backup.Spec.Source.RedisFailoverName != "" { - rf, err := r.K8SService.GetRedisFailover(ctx, backup.Namespace, backup.Spec.Source.RedisFailoverName) - if err != nil { - return err - } - if err := rf.Validate(); err != nil { - return err - } - job = generateBackupJob(backup, labels, ownerRefs, rf) - jobs, err := r.K8SService.ListJobsByLabel(ctx, backup.Namespace, labels) - if err == nil && len(jobs.Items) > 0 { - backup.Status.JobName = jobs.Items[0].Name - backup.Status.Condition = redisbackupv1.RedisBackupRunning - r.logger.Info("back up job is exists", "instance:", backup.Name) - return nil - } else if err != nil { - r.logger.Info(err.Error()) - } - } - if err := r.K8SService.CreateIfNotExistsJob(ctx, backup.Namespace, job); err != nil { - return err - } - backup.Status.JobName = job.Name - backup.Status.Condition = redisbackupv1.RedisBackupRunning - return nil -} - -func (r *RedisBackupKubeClient) EnsureBackupCompleted(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - if backup.Status.Condition != redisbackupv1.RedisBackupRunning { - return nil - } - // get job - job, err := r.K8SService.GetJob(context.TODO(), backup.Namespace, backup.Status.JobName) - if err != nil { - return err - } - // get job condition - if job.Status.Succeeded > 0 { - backup.Status.Condition = redisbackupv1.RedisBackupComplete - backup.Status.StartTime = job.Status.StartTime - backup.Status.CompletionTime = job.Status.CompletionTime - return nil - } - if job.Status.Failed > *job.Spec.BackoffLimit { - backup.Status.Condition = redisbackupv1.RedisBackupFailed - if len(job.Status.Conditions) > 0 { - backup.Status.Message = job.Status.Conditions[0].Message - } else { - backup.Status.Message = "Unknown" - } - return nil - } - // running - backup.Status.LastCheckTime = &metav1.Time{Time: metav1.Now().Time} - - return nil -} - -func (r *RedisBackupKubeClient) UpdateBackup(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - return r.K8SService.UpdateRedisBackup(ctx, backup) -} - -func (r *RedisBackupKubeClient) UpdateBackupStatus(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - return r.K8SService.UpdateRedisBackupStatus(ctx, backup) -} - -func (r *RedisBackupKubeClient) EnsureRoleReady(ctx context.Context, backup *redisbackupv1.RedisBackup) error { - // check sa - _, err := r.K8SService.GetServiceAccount(context.TODO(), backup.Namespace, util.RedisBackupServiceAccountName) - if err != nil { - if errors.IsNotFound(err) { - err := r.K8SService.CreateServiceAccount(context.TODO(), backup.Namespace, &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: util.RedisBackupServiceAccountName, - Namespace: backup.Namespace, - }, - }) - if err != nil { - return err - } - } else { - return err - } - } - - // check role - _, err = r.K8SService.GetRole(context.TODO(), backup.Namespace, util.RedisBackupRoleName) - if err != nil { - if errors.IsNotFound(err) { - err := r.K8SService.CreateRole(context.TODO(), backup.Namespace, &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: util.RedisBackupRoleName, - Namespace: backup.Namespace, - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{v1.GroupName}, - Resources: []string{"pods", "pods/exec"}, - Verbs: []string{"*"}, - }, - { - APIGroups: []string{redisbackupv1.GroupVersion.Group}, - Resources: []string{"*"}, - Verbs: []string{"*"}, - }, - }, - }) - if err != nil { - return err - } - } else { - return err - } - } - - // check role binding - _, err = r.K8SService.GetRoleBinding(context.TODO(), backup.Namespace, util.RedisBackupRoleBindingName) - if err != nil { - if errors.IsNotFound(err) { - err := r.K8SService.CreateRoleBinding(context.TODO(), backup.Namespace, &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: util.RedisBackupRoleBindingName, - Namespace: backup.Namespace, - }, - RoleRef: rbacv1.RoleRef{ - APIGroup: rbacv1.GroupName, - Kind: "Role", - Name: util.RedisBackupRoleName, - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: util.RedisBackupServiceAccountName, - Namespace: backup.Namespace, - }, - }, - }) - if err != nil { - return err - } - } else { - return err - } - } - return nil -} diff --git a/internal/controller/redis/redisbackup/service/client_test.go b/internal/controller/redis/redisbackup/service/client_test.go deleted file mode 100644 index fdff38e..0000000 --- a/internal/controller/redis/redisbackup/service/client_test.go +++ /dev/null @@ -1,157 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package service - -import ( - "testing" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestAddBackupAnnotationsAndLabels(t *testing.T) { - instance := &databasesv1.RedisFailover{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-failover", - }, - Status: databasesv1.RedisFailoverStatus{ - Phase: "Ready", - }, - Spec: databasesv1.RedisFailoverSpec{ - Redis: databasesv1.RedisSettings{ - Image: "redis:6.2.1", - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("500m"), - corev1.ResourceMemory: resource.MustParse("512Mi"), - }, - }, - }, - Sentinel: databasesv1.SentinelSettings{ - Replicas: 1, - }, - }, - } - backup := &redisbackupv1.RedisBackup{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-backup", - }, - Spec: redisbackupv1.RedisBackupSpec{ - Source: redisbackupv1.RedisBackupSource{ - RedisFailoverName: "redis-failover", - }, - }, - } - - err := AddBackupAnnotationsAndLabels(instance, backup) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Check annotations - if backup.Annotations == nil { - t.Error("Annotations should not be nil") - } - - expectedResources := "{\"requests\":{\"cpu\":\"500m\",\"memory\":\"512Mi\"}}" - if backup.Annotations["sourceResources"] != expectedResources { - t.Errorf("Expected sourceResources annotation: %s, but got: %s", expectedResources, backup.Annotations["sourceResources"]) - } - expectedVersion := "6.2.1" - if backup.Annotations["sourceClusterVersion"] != expectedVersion { - t.Errorf("Expected sourceClusterVersion annotation: 6.2.1, but got: %s", backup.Annotations["sourceClusterVersion"]) - } - if backup.Annotations["sourceSentinelReplicasMaster"] != "1" { - t.Errorf("Expected sourceSentinelReplicasMaster annotation: 1, but got: %s", backup.Annotations["sourceSentinelReplicasMaster"]) - } - if backup.Annotations["sourceSentinelReplicasMaster"] != "1" { - t.Errorf("Expected sourceSentinelReplicasMaster annotation: 1, but got: %s", backup.Annotations["sourceSentinelReplicasMaster"]) - } - if backup.Annotations["sourceSentinelReplicasSlave"] != "0" { - t.Errorf("Expected sourceSentinelReplicasSlave annotation: 0, but got: %s", backup.Annotations["sourceSentinelReplicasSlave"]) - } - - // Check labels - if backup.Labels == nil { - t.Error("Labels should not be nil") - } - if backup.Labels["redisfailovers.databases.spotahome.com/name"] != "redis-failover" { - t.Errorf("Expected redisfailovers.databases.spotahome.com/name label: redis-failover, but got: %s", backup.Labels["redisfailovers.databases.spotahome.com/name"]) - } -} - -func TestAddBackupAnnotationsAndLabels2(t *testing.T) { - instance := &databasesv1.RedisFailover{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-failover", - }, - Status: databasesv1.RedisFailoverStatus{ - Phase: "Ready", - }, - Spec: databasesv1.RedisFailoverSpec{ - - Redis: databasesv1.RedisSettings{}, - Sentinel: databasesv1.SentinelSettings{ - Replicas: 1, - }, - }, - } - backup := &redisbackupv1.RedisBackup{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-backup", - }, - Spec: redisbackupv1.RedisBackupSpec{ - Source: redisbackupv1.RedisBackupSource{ - RedisFailoverName: "redis-failover", - }, - }, - } - - err := AddBackupAnnotationsAndLabels(instance, backup) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Check annotations - if backup.Annotations == nil { - t.Error("Annotations should not be nil") - } - expectedResources := "{}" - if backup.Annotations["sourceResources"] != expectedResources { - t.Errorf("Expected sourceResources annotation: %s, but got: %s", expectedResources, backup.Annotations["sourceResources"]) - } - if backup.Annotations["sourceResources"] != expectedResources { - t.Errorf("Expected sourceResources annotation: %s, but got: %s", expectedResources, backup.Annotations["sourceResources"]) - } - if backup.Annotations["sourceSentinelReplicasMaster"] != "1" { - t.Errorf("Expected sourceSentinelReplicasMaster annotation: 1, but got: %s", backup.Annotations["sourceSentinelReplicasMaster"]) - } - if backup.Annotations["sourceSentinelReplicasSlave"] != "0" { - t.Errorf("Expected sourceSentinelReplicasSlave annotation: 0, but got: %s", backup.Annotations["sourceSentinelReplicasSlave"]) - } - - // Check labels - if backup.Labels == nil { - t.Error("Labels should not be nil") - } - if backup.Labels["redisfailovers.databases.spotahome.com/name"] != "redis-failover" { - t.Errorf("Expected redisfailovers.databases.spotahome.com/name label: redis-failover, but got: %s", backup.Labels["redisfailovers.databases.spotahome.com/name"]) - } -} diff --git a/internal/controller/redis/redisbackup/service/generator.go b/internal/controller/redis/redisbackup/service/generator.go deleted file mode 100644 index 9b311a3..0000000 --- a/internal/controller/redis/redisbackup/service/generator.go +++ /dev/null @@ -1,380 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package service - -import ( - "fmt" - - redisfailoverv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/util" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" -) - -func generateBackupConfigMap(backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference, - rf *redisfailoverv1.RedisFailover) *corev1.ConfigMap { - name := generatorJobConfigMapName(backup) - namespace := backup.Namespace - appendCommands := "" - if rf.Spec.EnableTLS { - appendCommands += util.GenerateRedisTLSOptions() - } - script_content := ` -set -e -master_ip=$(redis-cli -h %s -p %d %s --csv SENTINEL get-master-addr-by-name %s | tr ',' ' ' | tr -d '\"' |cut -d' ' -f1) -master_port=$(redis-cli -h %s -p %d %s --csv SENTINEL get-master-addr-by-name %s | tr ',' ' ' | tr -d '\"' |cut -d' ' -f2) - -if [ -f /backup/dump.rdb ] -then - echo 'dump.rdb exist, skip backup data' - exit 0 -fi - -if [ -f /redis-password/password ] -then - export REDIS_PASSWORD=$(cat /redis-password/password) -fi - -if [ ! -z "${REDIS_PASSWORD}" ]; then - redis-cli -a "${REDIS_PASSWORD}" -h "$master_ip" -p "$master_port" %s --rdb /backup/dump.rdb - echo "down finish" -else - redis-cli -h "$master_ip" -p "$master_port" %s --rdb /backup/dump.rdb - echo "down finish" -fi - -if [ ! -z "${S3_ENDPOINT}" ]; then - /opt/redis-tools backup push - echo "push s3 success" -fi -` - n := rand.Int() % len(backup.Spec.Source.Endpoint) - ipPort := backup.Spec.Source.Endpoint[n] - masterName := "mymaster" - if ipPort.MasterName != "" { - masterName = ipPort.MasterName - } - - Content := fmt.Sprintf(script_content, ipPort.Address, ipPort.Port, appendCommands, masterName, ipPort.Address, ipPort.Port, appendCommands, masterName, appendCommands, appendCommands) - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Data: map[string]string{ - "backup.sh": Content, - }, - } -} - -func generateBackupJobForS3(backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference, - rf *redisfailoverv1.RedisFailover) *batchv1.Job { - name := generatorJobNameForS3(backup) - namespace := backup.Namespace - - image := backup.Spec.Image - - executeMode := int32(0744) - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Spec: batchv1.JobSpec{ - BackoffLimit: backup.Spec.BackoffLimit, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - ActiveDeadlineSeconds: backup.Spec.ActiveDeadlineSeconds, - PriorityClassName: backup.Spec.PriorityClassName, - SecurityContext: backup.Spec.SecurityContext, - NodeSelector: backup.Spec.NodeSelector, - Tolerations: backup.Spec.Tolerations, - Affinity: backup.Spec.Affinity, - ServiceAccountName: util.RedisBackupServiceAccountName, - RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{ - { - Name: "backup", - Image: image, - ImagePullPolicy: "Always", - VolumeMounts: []corev1.VolumeMount{ - { - Name: "backup-data", - MountPath: "/backup", - }, - { - Name: util.S3SecretVolumeName, - MountPath: "/s3_secret", - ReadOnly: true, - }, - { - Name: "script-data", - MountPath: "/script", - }, - }, - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/script/backup.sh"}, - Resources: backup.Spec.Resources, - }, - }, - Volumes: []corev1.Volume{ - { - Name: "script-data", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: generatorJobConfigMapName(backup), - }, - DefaultMode: &executeMode, - }, - }, - }, - }, - }, - }, - }, - } - data_volume := corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}} - if backup.Spec.Source.StorageClassName != "" { - data_volume = corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: util.GetClaimName(backup.Status.Destination), - }, - } - } - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{Name: "backup-data", VolumeSource: data_volume}) - - if rf.Spec.Auth.SecretPath != "" { - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "redis-password", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{SecretName: rf.Spec.Auth.SecretPath}, - }, - }) - job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ - Name: "redis-password", - MountPath: "/redis-password", - ReadOnly: true, - }) - } - - if backup.Spec.Target.S3Option.S3Secret != "" { - s3secretVolumes := corev1.Volume{ - Name: util.S3SecretVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{SecretName: backup.Spec.Target.S3Option.S3Secret}, - }, - } - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, s3secretVolumes) - - job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{ - {Name: "S3_ENDPOINT", ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: backup.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_ENDPOINTURL, - }, - }}, - {Name: "S3_REGION", ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: backup.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_REGION, - }, - }}, - {Name: "DATA_DIR", Value: "/backup"}, - {Name: "S3_OBJECT_DIR", Value: backup.Spec.Target.S3Option.Dir}, - {Name: "S3_BUCKET_NAME", Value: backup.Spec.Target.S3Option.Bucket}, - }...) - } - - SSLSecretName := util.GetRedisSSLSecretName(rf.Name) - if backup.Spec.Source.SSLSecretName != "" { - SSLSecretName = backup.Spec.Source.SSLSecretName - } - if rf.Spec.EnableTLS { - job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ - Name: "redis-tls", - MountPath: "/tls", - }) - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "redis-tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: SSLSecretName, - }, - }, - }) - } - - return job -} - -func generateBackupJob(backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference, - rf *redisfailoverv1.RedisFailover) *batchv1.Job { - name := generatorJobName(backup) - namespace := backup.Namespace - image := config.GetDefaultBackupImage() - if backup.Spec.Image != "" { - image = backup.Spec.Image - } - - backoffLimit := int32(0) - - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Spec: batchv1.JobSpec{ - BackoffLimit: &backoffLimit, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - ServiceAccountName: util.RedisBackupServiceAccountName, - RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{ - { - Name: "backup", - Image: image, - Resources: backup.Spec.Resources, - ImagePullPolicy: "Always", - VolumeMounts: []corev1.VolumeMount{ - { - Name: "backup-data", - MountPath: "/backup", - }, - }, - Env: []corev1.EnvVar{ - {Name: "REDIS_NAME", - Value: backup.Spec.Source.RedisFailoverName, - }, - }, - - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup backup"}, - }, - }, - Volumes: []corev1.Volume{ - { - Name: "backup-data", - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: util.GetClaimName(backup.Status.Destination), - }, - }, - }, - }, - }, - }, - }, - } - - if rf.Spec.Auth.SecretPath != "" { - job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: "REDIS_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: rf.Spec.Auth.SecretPath, - }, - Key: config.RedisSecretPasswordKey, - }, - }, - }) - } - - // append redis cli commands - appendCommands := "" - if rf.Spec.EnableTLS { - appendCommands += util.GenerateRedisTLSOptions() - job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ - Name: "redis-tls", - MountPath: "/tls", - }) - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "redis-tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: util.GetRedisSSLSecretName(rf.Name), - }, - }, - }) - } - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - appendCommands += " -h ::1 " - } - job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: "APPEND_COMMANDS", - Value: appendCommands, - }) - return job -} - -func generatorJobName(rb *redisbackupv1.RedisBackup) string { - hashString := rand.String(5) - return fmt.Sprintf("%s-%s", rb.Name, hashString) -} - -func generatorPVC(backup *redisbackupv1.RedisBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) *corev1.PersistentVolumeClaim { - pvc := &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: generatorJobName(backup), - Namespace: backup.Namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: backup.Spec.Storage, - }, - }, - }, - } - if backup.Spec.Source.StorageClassName != "" { - pvc.Spec.StorageClassName = &backup.Spec.Source.StorageClassName - } - return pvc -} - -func generatorJobNameForS3(rb *redisbackupv1.RedisBackup) string { - - return fmt.Sprintf("%s-%s", "rb-job", rb.Name) -} - -func generatorJobConfigMapName(rb *redisbackupv1.RedisBackup) string { - return fmt.Sprintf("%s-%s", "rb", rb.Name) -} diff --git a/internal/controller/redis/redisbackup_controller.go b/internal/controller/redis/redisbackup_controller.go deleted file mode 100644 index c2ab03a..0000000 --- a/internal/controller/redis/redisbackup_controller.go +++ /dev/null @@ -1,156 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redis - -import ( - "context" - "time" - - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - - middlewarev1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/internal/controller/redis/redisbackup" - "github.com/alauda/redis-operator/internal/controller/redis/redisbackup/service" - k8s "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/clientset" - "github.com/go-logr/logr" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -// RedisBackupReconciler reconciles a RedisBackup object -type RedisBackupReconciler struct { - client.Client - k8sClient k8s.ClientSet - handler *redisbackup.RedisBackupHandler - - Logger logr.Logger - Scheme *runtime.Scheme -} - -const RedisBackupFinalizer = "redisbackups.redis.middleware.alauda.io/finalizer" - -//+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisbackups,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisbackups/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisbackups/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile -func (r *RedisBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - - obj := middlewarev1.RedisBackup{} - if err := r.Client.Get(ctx, req.NamespacedName, &obj); err != nil { - if errors.IsNotFound(err) { - return ctrl.Result{}, nil - } - logger.Error(err, "get instance failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - isMarkedToBeDeleted := obj.GetDeletionTimestamp() != nil - if isMarkedToBeDeleted { - logger.Info("finalizeRedisBackup", "Namespace", obj.Namespace, "Instance", obj.Name) - if err := r.finalizeRedisBackup(ctx, logger, &obj); err != nil { - if obj.Status.Message != err.Error() { - obj.Status.Condition = middlewarev1.RedisDeleteFailed - obj.Status.Message = err.Error() - if _err := r.k8sClient.UpdateRedisBackupStatus(ctx, &obj); _err != nil { - logger.Error(_err, "update backup status failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - } - return ctrl.Result{}, err - } else { - logger.Info("RemoveFinalizer", "Namespace", obj.Namespace, "Instance", obj.Name) - controllerutil.RemoveFinalizer(&obj, RedisBackupFinalizer) - err := r.Update(ctx, &obj) - if err != nil { - return ctrl.Result{}, err - } - } - return ctrl.Result{}, nil - } - if err := obj.Validate(); err != nil { - logger.Error(err, "validate failed", "instance", req.NamespacedName) - obj.Status.Condition = middlewarev1.RedisBackupFailed - obj.Status.Message = err.Error() - if err := r.k8sClient.UpdateRedisBackupStatus(ctx, &obj); err != nil { - logger.Error(err, "update backup status failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - return ctrl.Result{}, nil - } - - if err := r.handler.Ensure(ctx, &obj); err != nil { - return ctrl.Result{}, err - } - - obj = middlewarev1.RedisBackup{} - if err := r.Client.Get(ctx, req.NamespacedName, &obj); err != nil { - if errors.IsNotFound(err) { - return ctrl.Result{}, nil - } - logger.Error(err, "get instance failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - - if !controllerutil.ContainsFinalizer(&obj, RedisBackupFinalizer) { - controllerutil.AddFinalizer(&obj, RedisBackupFinalizer) - err := r.Update(ctx, &obj) - if err != nil { - return ctrl.Result{}, err - } - } - - if obj.Status.Condition == middlewarev1.RedisBackupRunning { - return ctrl.Result{RequeueAfter: time.Second * 10}, nil - } - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *RedisBackupReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.SetupHandler(mgr) - - return ctrl.NewControllerManagedBy(mgr). - For(&middlewarev1.RedisBackup{}). - Complete(r) -} - -// SetupHandler -func (r *RedisBackupReconciler) SetupHandler(mgr ctrl.Manager) { - r.k8sClient = clientset.New(mgr.GetClient(), r.Logger) - - client := service.NewRedisBackupKubeClient(r.k8sClient, r.Logger) - r.handler = redisbackup.NewRedisBackupHandler(r.k8sClient, client, r.Logger) -} - -func (r *RedisBackupReconciler) finalizeRedisBackup(ctx context.Context, reqLogger logr.Logger, backup *middlewarev1.RedisBackup) error { - if backup.Spec.Target.S3Option.S3Secret != "" { - if err := r.handler.DeleteS3(ctx, backup); err != nil { - r.Logger.Info("DeleteS3", "Err", err.Error()) - return err - } - } - return nil -} diff --git a/internal/controller/redis/redisclusterbackup/handler.go b/internal/controller/redis/redisclusterbackup/handler.go deleted file mode 100644 index 0fc415e..0000000 --- a/internal/controller/redis/redisclusterbackup/handler.go +++ /dev/null @@ -1,179 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redisclusterbackup - -import ( - "context" - "errors" - "net/url" - "reflect" - - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/internal/controller/redis/redisclusterbackup/service" - "github.com/alauda/redis-operator/pkg/config" - k8s "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - rfLabelNameKey = "redisclusterbackups.redis.middleware.alauda.io/name" - labelInstanceKey = "redisclusterbackups.redis.middleware.alauda.io/instanceName" -) - -var ( - defaultLabels = map[string]string{ - "managed-by": "redis-cluster-operator", - } -) - -type RedisClusterBackupHandler struct { - backupService service.RedisClusterBackupClient - k8sservices k8s.ClientSet - logger logr.Logger -} - -func NewRedisClusterBackupHandler(k8sservice k8s.ClientSet, backupService service.RedisClusterBackupClient, logger logr.Logger) *RedisClusterBackupHandler { - return &RedisClusterBackupHandler{ - k8sservices: k8sservice, - backupService: backupService, - logger: logger, - } -} - -func (r *RedisClusterBackupHandler) Ensure(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error { - or := createOwnerReferences(backup) - // Create the labels every object derived from this need to have. - labels := getLabels(backup) - - if err := r.backupService.EnsureInfoAnnotationsAndLabels(ctx, backup); err != nil { - return err - } - - if err := r.backupService.EnsureRoleReady(ctx, backup); err != nil { - return err - } - if err := r.backupService.EnsureStorageReady(ctx, backup, labels, or); err != nil { - return err - } - if err := r.backupService.EnsureBackupJobCreated(ctx, backup, labels, or); err != nil { - r.logger.Info("EnsureBackupJobCreated", "err", err.Error()) - return err - } - if err := r.backupService.EnsureBackupCompleted(ctx, backup); err != nil { - r.logger.Info("EnsureBackupCompleted", "err", err.Error()) - return err - } - if err := r.backupService.UpdateBackupStatus(ctx, backup); err != nil { - r.logger.Info("UpdateBackupStatus", "err", err.Error()) - return err - } - return nil -} - -func getLabels(cluster *redisbackupv1.RedisClusterBackup) map[string]string { - dynLabels := map[string]string{ - rfLabelNameKey: cluster.Name, - labelInstanceKey: cluster.Spec.Source.RedisClusterName, - } - return util.MergeMap(defaultLabels, dynLabels) -} - -func createOwnerReferences(rf *redisbackupv1.RedisClusterBackup) []metav1.OwnerReference { - t := reflect.TypeOf(&redisbackupv1.RedisClusterBackup{}).Elem() - ownerRef := metav1.NewControllerRef(rf, redisbackupv1.GroupVersion.WithKind(t.Name())) - ownerRef.BlockOwnerDeletion = nil - ownerRef.Controller = nil - return []metav1.OwnerReference{*ownerRef} -} - -func (r *RedisClusterBackupHandler) DeleteS3(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error { - if backup.Spec.Target.S3Option.S3Secret == "" { - return nil - } - secret, err := r.k8sservices.GetSecret(ctx, backup.Namespace, backup.Spec.Target.S3Option.S3Secret) - if err != nil { - return err - } - token := "" - if v, ok := secret.Data[config.S3_TOKEN]; ok { - token = string(v) - } - secure := false - s3_url := string(secret.Data[config.S3_ENDPOINTURL]) - endpoint, err := url.Parse(s3_url) - if err != nil { - return err - } - if endpoint.Scheme == "https" { - secure = true - } - - s3Client, err := minio.New(endpoint.Host, - &minio.Options{ - Creds: credentials.NewStaticV4(string(secret.Data[config.S3_ACCESS_KEY_ID]), string(secret.Data[config.S3_SECRET_ACCESS_KEY]), token), - Region: string(secret.Data[config.S3_REGION]), - Secure: secure, - }) - if err != nil { - return err - } - if exists, err := s3Client.BucketExists(ctx, backup.Spec.Target.S3Option.Bucket); err != nil { - return err - } else if !exists { - return errors.New("S3 Bucket no exists") - } - var err_list []error - objectsCh := make(chan minio.ObjectInfo) - - go func() { - defer close(objectsCh) - opts := minio.ListObjectsOptions{Prefix: backup.Spec.Target.S3Option.Dir, Recursive: true} - for object := range s3Client.ListObjects(ctx, backup.Spec.Target.S3Option.Bucket, opts) { - if object.Err != nil { - r.logger.Info("ListObjects", "Err", object.Err.Error()) - err_list = append(err_list, object.Err) - } - objectsCh <- object - } - }() - - errorCh := s3Client.RemoveObjects(ctx, backup.Spec.Target.S3Option.Bucket, objectsCh, minio.RemoveObjectsOptions{}) - for e := range errorCh { - if e.Err != nil { - return err - } - } - if len(err_list) != 0 { - err_text := "ListObjects Err:" - for _, v := range err_list { - err_text += v.Error() - } - return errors.New(err_text) - } - if len(err_list) != 0 { - err_text := "ListObjects Err:" - for _, v := range err_list { - err_text += v.Error() - } - return errors.New(err_text) - } - return nil -} diff --git a/internal/controller/redis/redisclusterbackup/service/client.go b/internal/controller/redis/redisclusterbackup/service/client.go deleted file mode 100644 index c69d091..0000000 --- a/internal/controller/redis/redisclusterbackup/service/client.go +++ /dev/null @@ -1,301 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package service - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/config" - k8s "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type RedisClusterBackupClient interface { - EnsureInfoAnnotationsAndLabels(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error - EnsureStorageReady(ctx context.Context, backup *redisbackupv1.RedisClusterBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error - EnsureBackupJobCreated(ctx context.Context, backup *redisbackupv1.RedisClusterBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error - EnsureBackupCompleted(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error - UpdateBackup(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error - UpdateBackupStatus(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error - EnsureRoleReady(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error -} - -type RedisClusterBackupKubeClient struct { - K8SService k8s.ClientSet - logger logr.Logger -} - -func NewRedisClusterBackupKubeClient(k8sService k8s.ClientSet, logger logr.Logger) *RedisClusterBackupKubeClient { - return &RedisClusterBackupKubeClient{ - K8SService: k8sService, - logger: logger, - } -} - -func (r *RedisClusterBackupKubeClient) EnsureInfoAnnotationsAndLabels(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error { - instance, err := r.K8SService.GetDistributedRedisCluster(ctx, backup.Namespace, backup.Spec.Source.RedisClusterName) - if err != nil { - return err - } - if instance.Status.Reason != "OK" && instance.Status.Status != v1alpha1.ClusterStatusOK { - return fmt.Errorf("sentinel cluster :%s, is not Ready", instance.Name) - } - if backup.Annotations == nil { - backup.Annotations = map[string]string{} - } - err = backup.Validate() - if err != nil { - return err - } - backup.Annotations["sourceClusterReplicasShard"] = strconv.Itoa(int(instance.Spec.MasterSize)) - backup.Annotations["sourceClusterReplicasSlave"] = strconv.Itoa(int(instance.Spec.ClusterReplicas)) - - backup.Annotations["sourceClusterVersion"] = config.GetRedisVersion(instance.Spec.Image) - if !instance.Spec.Resources.Limits.Memory().IsZero() { - size := resource.NewQuantity(instance.Spec.Resources.Limits.Memory().Value()*int64(instance.Spec.MasterSize), resource.BinarySI) - if size.Cmp(backup.Spec.Storage) > 0 { - backup.Spec.Storage = *size - } - } - res, _ := json.Marshal(instance.Spec.Resources) - backup.Annotations["sourceResources"] = string(res) - if backup.Labels == nil { - backup.Labels = map[string]string{} - } - backup.Labels["redis.kun/name"] = backup.Spec.Source.RedisClusterName - return r.UpdateBackup(ctx, backup) -} - -func (r *RedisClusterBackupKubeClient) EnsureStorageReady(ctx context.Context, backup *redisbackupv1.RedisClusterBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error { - if backup.Status.Destination != "" { - return nil - } - if backup.Spec.Source.StorageClassName == "" { - return nil - } - pvc := generatorPVC(backup, labels, ownerRefs) - old_pvcs, err := r.K8SService.ListPvcByLabel(ctx, pvc.Namespace, labels) - if err != nil { - return err - } - if old_pvcs == nil || len(old_pvcs.Items) == 0 { - err := r.K8SService.CreatePVC(ctx, backup.Namespace, pvc) - if err != nil { - return err - } - backup.Status.Destination = fmt.Sprintf("%s/%s", "pvc", pvc.Name) - } else if len(old_pvcs.Items) > 0 { - backup.Status.Destination = fmt.Sprintf("%s/%s", "pvc", old_pvcs.Items[0].Name) - } - return nil -} - -func (r *RedisClusterBackupKubeClient) EnsureBackupJobCreated(ctx context.Context, backup *redisbackupv1.RedisClusterBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) error { - if backup.Status.JobName != "" { - return nil - } - cluster, err := r.K8SService.GetDistributedRedisCluster(ctx, backup.Namespace, backup.Spec.Source.RedisClusterName) - - if err != nil { - return nil - } - err = cluster.Init() - if err != nil { - r.logger.Error(err, "init cluster error") - } - job := &batchv1.Job{} - if backup.Spec.Target.S3Option.S3Secret != "" { - err = backup.Validate() - if err != nil { - return err - } - config_map := generateBackupConfigMap(backup, labels, ownerRefs, cluster) - err := r.K8SService.CreateIfNotExistsConfigMap(ctx, backup.Namespace, config_map) - if err != nil { - return err - } - job = generateBackupJobForS3(backup, labels, ownerRefs, cluster) - } else if backup.Spec.Source.RedisClusterName != "" { - err = backup.Validate() - if err != nil { - return err - } - if cluster.Spec.MasterSize != cluster.Status.NumberOfMaster { - return fmt.Errorf("cluster %s is not ready", cluster.Name) - } - jobs, err := r.K8SService.ListJobsByLabel(ctx, backup.Namespace, labels) - if err == nil && len(jobs.Items) > 0 { - backup.Status.JobName = jobs.Items[0].Name - backup.Status.Condition = redisbackupv1.RedisBackupRunning - r.logger.Info("back up job is exists", "instance:", backup.Name) - return nil - } else if err != nil { - r.logger.Info(err.Error()) - } - job = generateBackupJob(backup, cluster, labels, ownerRefs) - - } else { - return fmt.Errorf("backup source is not valid") - } - jobs, err := r.K8SService.ListJobsByLabel(ctx, backup.Namespace, labels) - if err == nil && len(jobs.Items) > 0 { - backup.Status.JobName = jobs.Items[0].Name - backup.Status.Condition = redisbackupv1.RedisBackupRunning - r.logger.Info("back up job is exists", "instance:", backup.Name) - return nil - } else if err != nil { - r.logger.Info(err.Error()) - } - - err = r.K8SService.CreateIfNotExistsJob(ctx, backup.Namespace, job) - if err != nil { - return err - } - - backup.Status.JobName = job.Name - backup.Status.Condition = redisbackupv1.RedisBackupRunning - - return nil -} - -func (r *RedisClusterBackupKubeClient) EnsureBackupCompleted(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error { - if backup.Status.Condition != redisbackupv1.RedisBackupRunning { - return nil - } - job, err := r.K8SService.GetJob(ctx, backup.Namespace, backup.Status.JobName) - if err != nil { - return err - } - if job.Status.Succeeded > 0 { - backup.Status.Condition = redisbackupv1.RedisBackupComplete - backup.Status.StartTime = job.Status.StartTime - backup.Status.CompletionTime = job.Status.CompletionTime - return nil - } - if job.Status.Failed > *job.Spec.BackoffLimit { - backup.Status.Condition = redisbackupv1.RedisBackupFailed - if len(job.Status.Conditions) > 0 { - backup.Status.Message = job.Status.Conditions[0].Message - } else { - backup.Status.Message = "Unknown" - } - return nil - } - // running - backup.Status.LastCheckTime = &metav1.Time{Time: metav1.Now().Time} - return nil -} - -func (r *RedisClusterBackupKubeClient) UpdateBackup(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error { - return r.K8SService.UpdateRedisClusterBackup(ctx, backup) -} - -func (r *RedisClusterBackupKubeClient) UpdateBackupStatus(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error { - return r.K8SService.UpdateRedisClusterBackupStatus(ctx, backup) -} - -func (r *RedisClusterBackupKubeClient) EnsureRoleReady(ctx context.Context, backup *redisbackupv1.RedisClusterBackup) error { - // check sa - _, err := r.K8SService.GetServiceAccount(context.TODO(), backup.Namespace, util.RedisBackupServiceAccountName) - if err != nil { - if errors.IsNotFound(err) { - err := r.K8SService.CreateServiceAccount(context.TODO(), backup.Namespace, &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: util.RedisBackupServiceAccountName, - Namespace: backup.Namespace, - }, - }) - if err != nil { - return err - } - } else { - return err - } - } - // check role - _, err = r.K8SService.GetRole(context.TODO(), backup.Namespace, util.RedisBackupRoleName) - if err != nil { - if errors.IsNotFound(err) { - err := r.K8SService.CreateRole(context.TODO(), backup.Namespace, &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: util.RedisBackupRoleName, - Namespace: backup.Namespace, - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{corev1.GroupName}, - Resources: []string{"pods", "pods/exec"}, - Verbs: []string{"*"}, - }, - { - APIGroups: []string{redisbackupv1.GroupVersion.Group}, - Resources: []string{"*"}, - Verbs: []string{"*"}, - }, - }, - }) - if err != nil { - return err - } - } else { - return err - } - } - - // check role binding - _, err = r.K8SService.GetRoleBinding(context.TODO(), backup.Namespace, util.RedisBackupRoleBindingName) - if err != nil { - if errors.IsNotFound(err) { - err := r.K8SService.CreateRoleBinding(context.TODO(), backup.Namespace, &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: util.RedisBackupRoleBindingName, - Namespace: backup.Namespace, - }, - RoleRef: rbacv1.RoleRef{ - APIGroup: rbacv1.GroupName, - Kind: "Role", - Name: util.RedisBackupRoleName, - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: util.RedisBackupServiceAccountName, - Namespace: backup.Namespace, - }, - }, - }) - if err != nil { - return err - } - } else { - return err - } - } - return nil -} diff --git a/internal/controller/redis/redisclusterbackup/service/generator.go b/internal/controller/redis/redisclusterbackup/service/generator.go deleted file mode 100644 index b910bb6..0000000 --- a/internal/controller/redis/redisclusterbackup/service/generator.go +++ /dev/null @@ -1,383 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package service - -import ( - "fmt" - "strconv" - - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redisbackupv1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/util" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" -) - -func generatorPVC(backup *redisbackupv1.RedisClusterBackup, labels map[string]string, ownerRefs []metav1.OwnerReference) *corev1.PersistentVolumeClaim { - pvc := &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: generatorJobName(backup), - Namespace: backup.Namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: backup.Spec.Storage, - }, - }, - }, - } - if backup.Spec.Source.StorageClassName != "" { - pvc.Spec.StorageClassName = &backup.Spec.Source.StorageClassName - } - return pvc -} - -func generatorJobName(rb *redisbackupv1.RedisClusterBackup) string { - hashString := rand.String(5) - return fmt.Sprintf("%s-%s", rb.Name, hashString) -} - -func generateBackupJob(backup *redisbackupv1.RedisClusterBackup, drc *v1alpha1.DistributedRedisCluster, labels map[string]string, ownerRefs []metav1.OwnerReference) *batchv1.Job { - name := generatorJobName(backup) - namespace := backup.Namespace - - image := config.GetDefaultBackupImage() - if backup.Spec.Image != "" { - image = backup.Spec.Image - } - backoffLimit := int32(0) - privileged := false - - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Spec: batchv1.JobSpec{ - BackoffLimit: &backoffLimit, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - ServiceAccountName: util.RedisBackupServiceAccountName, - RestartPolicy: corev1.RestartPolicyNever, - Volumes: []corev1.Volume{ - { - Name: "backup-data", - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: util.GetClaimName(backup.Status.Destination), - }, - }, - }, - }, - }, - }, - }, - } - if drc.Spec.EnableTLS { - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "redis-tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: util.GetRedisSSLSecretName(drc.Name), - }, - }, - }) - } - - i := 0 - for _, node := range drc.Status.Nodes { - if node.Role != "Master" { - continue - } - container := corev1.Container{ - Name: fmt.Sprintf("backup-%d", i), - Image: image, - ImagePullPolicy: "Always", - Resources: *backup.Spec.Resources, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "backup-data", - MountPath: "/backup", - }, - }, - Env: []corev1.EnvVar{{Name: "REDIS_NAME", Value: node.PodName}, - {Name: "REDIS_ClUSTER_INDEX", Value: strconv.Itoa(i)}, - }, - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup backup"}, - SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - }, - } - - if drc.Spec.PasswordSecret != nil && drc.Spec.PasswordSecret.Name != "" { - container.Env = append(container.Env, corev1.EnvVar{ - Name: "REDIS_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: drc.Spec.PasswordSecret.Name, - }, - Key: config.RedisSecretPasswordKey, - }, - }, - }) - } - - // append redis cli commands - appendCommands := "" - if drc.Spec.EnableTLS { - appendCommands += util.GenerateRedisTLSOptions() - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: "redis-tls", - MountPath: "/tls", - }) - } - if drc.Spec.IPFamilyPrefer == corev1.IPv6Protocol { - appendCommands += " -h ::1 " - } - container.Env = append(container.Env, corev1.EnvVar{ - Name: "APPEND_COMMANDS", - Value: appendCommands, - }) - job.Spec.Template.Spec.Containers = append(job.Spec.Template.Spec.Containers, container) - i++ - } - - return job -} - -func generatorJobNameS3(rb *redisbackupv1.RedisClusterBackup) string { - return fmt.Sprintf("%s-%s", "rbc-job", rb.Name) -} - -func generatorJobConfigMapName(rb *redisbackupv1.RedisClusterBackup) string { - return fmt.Sprintf("%s-%s", "rbc", rb.Name) -} - -func generateBackupJobForS3(backup *redisbackupv1.RedisClusterBackup, labels map[string]string, - ownerRefs []metav1.OwnerReference, rc *v1alpha1.DistributedRedisCluster) *batchv1.Job { - - name := generatorJobNameS3(backup) - namespace := backup.Namespace - - image := backup.Spec.Image - if image == "" { - image = config.GetDefaultBackupImage() - } - executeMode := int32(0744) - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Spec: batchv1.JobSpec{ - BackoffLimit: backup.Spec.BackoffLimit, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - ServiceAccountName: util.RedisBackupServiceAccountName, - ActiveDeadlineSeconds: backup.Spec.ActiveDeadlineSeconds, - PriorityClassName: backup.Spec.PriorityClassName, - SecurityContext: backup.Spec.SecurityContext, - NodeSelector: backup.Spec.NodeSelector, - Tolerations: backup.Spec.Tolerations, - Affinity: backup.Spec.Affinity, - RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{ - { - Name: "backup", - Image: image, - Resources: *backup.Spec.Resources, - ImagePullPolicy: "Always", - VolumeMounts: []corev1.VolumeMount{ - { - Name: "backup-data", - MountPath: "/backup", - }, { - Name: "script-data", - MountPath: "/script", - }, { - Name: "s3-secret", - MountPath: "/s3_secret", - }, - }, - Env: []corev1.EnvVar{{Name: "DATA_DIR", Value: "/backup"}}, - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/script/backup.sh"}, - }, - }, - Volumes: []corev1.Volume{ - { - Name: "script-data", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: generatorJobConfigMapName(backup), - }, - DefaultMode: &executeMode, - }, - }, - }, - }, - }, - }, - }, - } - data_volume := corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}} - if backup.Spec.Source.StorageClassName != "" { - data_volume = corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: util.GetClaimName(backup.Status.Destination), - }, - } - } - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{Name: "backup-data", VolumeSource: data_volume}) - - if rc.Spec.PasswordSecret != nil && rc.Spec.PasswordSecret.Name != "" { - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "redis-password", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{SecretName: rc.Spec.PasswordSecret.Name}, - }, - }) - job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ - Name: "redis-password", - MountPath: "/redis-password", - ReadOnly: true, - }) - } - - if backup.Spec.Target.S3Option.S3Secret != "" { - s3secretVolumes := corev1.Volume{ - Name: "s3-secret", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{SecretName: backup.Spec.Target.S3Option.S3Secret}, - }, - } - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, s3secretVolumes) - job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{ - {Name: "S3_ENDPOINT", ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: backup.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_ENDPOINTURL, - }, - }}, - {Name: "S3_REGION", ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: backup.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_REGION, - }, - }}, - {Name: "S3_OBJECT_DIR", Value: backup.Spec.Target.S3Option.Dir}, - {Name: "S3_BUCKET_NAME", Value: backup.Spec.Target.S3Option.Bucket}, - }...) - } - - SSLSecretName := util.GetRedisSSLSecretName(rc.Name) - if backup.Spec.Source.SSLSecretName != "" { - SSLSecretName = backup.Spec.Source.SSLSecretName - } - if rc.Spec.EnableTLS { - job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ - Name: "redis-tls", - MountPath: "/tls", - }) - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "redis-tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: SSLSecretName, - }, - }, - }) - } - - return job -} - -func generateBackupConfigMap(backup *redisbackupv1.RedisClusterBackup, labels map[string]string, ownerRefs []metav1.OwnerReference, - rc *v1alpha1.DistributedRedisCluster) *corev1.ConfigMap { - name := generatorJobConfigMapName(backup) - namespace := backup.Namespace - appendCommands := "" - if rc.Spec.EnableTLS { - appendCommands += util.GenerateRedisTLSOptions() - } - script_content := ` -set -e -if [ -f /redis-password/password ] -then - export REDIS_PASSWORD=$(cat /redis-password/password) -fi -if [ -f /backup/nodes.json ] -then - echo 'nodes.json exist, skip backup data' - exit 0 -fi -if [ ! -z "${REDIS_PASSWORD}" ]; then - redis-cli -a "${REDIS_PASSWORD}" %s --cluster backup %s:%d /backup - echo "down finish" -else - redis-cli %s --cluster backup %s:%d /backup - echo "down finish" -fi - -echo "start rename cluster file" -/opt/redis-tools backup rename - -if [ ! -z "${S3_ENDPOINT}" ]; then - echo "start push s3 " - /opt/redis-tools backup push - echo "push s3 success" -fi -` - n := rand.Int() % len(backup.Spec.Source.Endpoint) - ipPort := backup.Spec.Source.Endpoint[n] - Content := fmt.Sprintf(script_content, appendCommands, ipPort.Address, ipPort.Port, appendCommands, ipPort.Address, ipPort.Port) - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: ownerRefs, - }, - Data: map[string]string{ - "backup.sh": Content, - }, - } -} diff --git a/internal/controller/redis/redisclusterbackup_controller.go b/internal/controller/redis/redisclusterbackup_controller.go deleted file mode 100644 index 05aec73..0000000 --- a/internal/controller/redis/redisclusterbackup_controller.go +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redis - -import ( - "context" - "time" - - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" - - middlewarev1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/internal/controller/redis/redisclusterbackup" - "github.com/alauda/redis-operator/internal/controller/redis/redisclusterbackup/service" - k8s "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/clientset" - "github.com/go-logr/logr" -) - -// RedisBackupReconciler reconciles a RedisBackup object -type RedisClusterBackupReconciler struct { - client.Client - k8sClient k8s.ClientSet - handler *redisclusterbackup.RedisClusterBackupHandler - Logger logr.Logger - Scheme *runtime.Scheme -} - -const RedisClusterBackupFinalizer = "redisclusterbackups.redis.middleware.alauda.io/finalizer" - -//+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisclusterbackups,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisclusterbackups/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=redis.middleware.alauda.io,resources=redisclusterbackups/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile -func (r *RedisClusterBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - - obj := middlewarev1.RedisClusterBackup{} - if err := r.Client.Get(ctx, req.NamespacedName, &obj); err != nil { - if errors.IsNotFound(err) { - logger.Error(err, "get instance failed", "instance", req.NamespacedName) - return ctrl.Result{}, nil - } - logger.Error(err, "get instance failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - isMarkedToBeDeleted := obj.GetDeletionTimestamp() != nil - if isMarkedToBeDeleted { - logger.Info("finalizeRedisBackup", "Namespace", obj.Namespace, "Instance", obj.Name) - if err := r.finalizeRedisBackup(ctx, logger, &obj); err != nil { - if obj.Status.Message != err.Error() { - obj.Status.Condition = middlewarev1.RedisDeleteFailed - obj.Status.Message = err.Error() - if _err := r.k8sClient.UpdateRedisClusterBackupStatus(ctx, &obj); _err != nil { - logger.Error(_err, "update backup status failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - } - return ctrl.Result{}, err - } else { - logger.Info("RemoveFinalizer", "Namespace", obj.Namespace, "Instance", obj.Name) - controllerutil.RemoveFinalizer(&obj, RedisBackupFinalizer) - err := r.Update(ctx, &obj) - if err != nil { - return ctrl.Result{}, err - } - } - return ctrl.Result{}, nil - } - if err := obj.Validate(); err != nil { - logger.Error(err, "validate failed", "instance", req.NamespacedName) - obj.Status.Condition = middlewarev1.RedisBackupFailed - obj.Status.Message = err.Error() - if err := r.k8sClient.UpdateRedisClusterBackupStatus(ctx, &obj); err != nil { - logger.Error(err, "update backup status failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - - return ctrl.Result{}, nil - } - - if err := r.handler.Ensure(ctx, &obj); err != nil { - return ctrl.Result{}, err - } - - obj = middlewarev1.RedisClusterBackup{} - if err := r.Client.Get(ctx, req.NamespacedName, &obj); err != nil { - if errors.IsNotFound(err) { - return ctrl.Result{}, nil - } - logger.Error(err, "get instance failed", "instance", req.NamespacedName) - return ctrl.Result{}, err - } - - if !controllerutil.ContainsFinalizer(&obj, RedisBackupFinalizer) { - controllerutil.AddFinalizer(&obj, RedisBackupFinalizer) - err := r.Update(ctx, &obj) - if err != nil { - return ctrl.Result{}, err - } - } - - if obj.Status.Condition == middlewarev1.RedisBackupRunning { - return ctrl.Result{RequeueAfter: time.Second * 10}, nil - } - - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *RedisClusterBackupReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.SetupHandler(mgr) - - return ctrl.NewControllerManagedBy(mgr). - For(&middlewarev1.RedisClusterBackup{}). - Complete(r) -} - -// SetupHandler -func (r *RedisClusterBackupReconciler) SetupHandler(mgr ctrl.Manager) { - r.k8sClient = clientset.New(mgr.GetClient(), r.Logger) - client := service.NewRedisClusterBackupKubeClient(r.k8sClient, r.Logger) - r.handler = redisclusterbackup.NewRedisClusterBackupHandler(r.k8sClient, client, r.Logger) -} - -func (r *RedisClusterBackupReconciler) finalizeRedisBackup(ctx context.Context, reqLogger logr.Logger, backup *middlewarev1.RedisClusterBackup) error { - if backup.Spec.Target.S3Option.S3Secret != "" { - if err := r.handler.DeleteS3(ctx, backup); err != nil { - reqLogger.Info("DeleteS3", "Err", err.Error()) - return err - } - } - - return nil -} diff --git a/internal/controller/redis/redisuser/handler.go b/internal/controller/redis/redisuser/handler.go deleted file mode 100644 index 11874f0..0000000 --- a/internal/controller/redis/redisuser/handler.go +++ /dev/null @@ -1,258 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package redisuser - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" - "k8s.io/apimachinery/pkg/api/errors" - - redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - clustermodel "github.com/alauda/redis-operator/pkg/models/cluster" - sentinelmodle "github.com/alauda/redis-operator/pkg/models/sentinel" - "github.com/go-logr/logr" - "k8s.io/utils/strings/slices" -) - -type RedisUserHandler struct { - k8sClient kubernetes.ClientSet - logger logr.Logger -} - -func NewRedisUserHandler(k8sservice kubernetes.ClientSet, logger logr.Logger) *RedisUserHandler { - return &RedisUserHandler{ - k8sClient: k8sservice, - logger: logger, - } -} - -func (r *RedisUserHandler) Delete(ctx context.Context, instance redismiddlewarealaudaiov1.RedisUser) error { - r.logger.V(3).Info("redis user delete", "redis user name", instance.Name, "type", instance.Spec.Arch) - switch instance.Spec.Arch { - case redis.ClusterArch: - r.logger.V(3).Info("cluster", "redis name", instance.Spec.RedisName) - rc, err := r.k8sClient.GetDistributedRedisCluster(ctx, instance.Namespace, instance.Spec.RedisName) - if err != nil { - if errors.IsNotFound(err) { - r.logger.Info("redis instance is not found", "redis name", instance.Spec.RedisName) - return nil - } - return err - } - rcm, err := clustermodel.NewRedisCluster(ctx, r.k8sClient, rc, r.logger) - if !rcm.IsReady() { - r.logger.Info("redis instance is not ready", "redis name", instance.Spec.RedisName) - return fmt.Errorf("redis instance is not ready") - } - if err != nil { - return err - } - - for _, node := range rcm.Nodes() { - err := node.Setup(ctx, []interface{}{"ACL", "DELUSER", instance.Spec.Username}) - if err != nil { - r.logger.Error(err, "acl del user failed", "node", node.GetName()) - return err - } - r.logger.Info("acl del user success", "node", node.GetName()) - } - configMap, err := r.k8sClient.GetConfigMap(ctx, instance.Namespace, clusterbuilder.GenerateClusterACLConfigMapName(instance.Spec.RedisName)) - if err != nil { - if errors.IsNotFound(err) { - return nil - } - return err - } - delete(configMap.Data, instance.Spec.Username) - if err := r.k8sClient.UpdateConfigMap(ctx, instance.Namespace, configMap); err != nil { - return err - } - case redis.SentinelArch: - r.logger.V(3).Info("sentinel", "redis name", instance.Spec.RedisName) - rf, err := r.k8sClient.GetRedisFailover(ctx, instance.Namespace, instance.Spec.RedisName) - if err != nil { - if errors.IsNotFound(err) { - r.logger.Info("redis instance is not found", "redis name", instance.Spec.RedisName) - return nil - } - return err - } - - rfm, err := sentinelmodle.NewRedisFailover(ctx, r.k8sClient, rf, r.logger) - if !rfm.IsReady() { - r.logger.Info("redis instance is not ready", "redis name", instance.Spec.RedisName) - return fmt.Errorf("redis instance is not ready") - } - if err != nil { - return err - } - for _, node := range rfm.Nodes() { - err := node.Setup(ctx, []interface{}{"ACL", "DELUSER", instance.Spec.Username}) - if err != nil { - r.logger.Error(err, "acl del user failed", "node", node.GetName()) - return err - } - r.logger.Info("acl del user success", "node", node.GetName()) - } - - } - return nil -} - -func (r *RedisUserHandler) Do(ctx context.Context, instance redismiddlewarealaudaiov1.RedisUser) error { - passwords := []string{} - // operators account skip - if instance.Spec.AccountType == redismiddlewarealaudaiov1.System { - return nil - } - password_obj := &user.Password{} - for _, secretName := range instance.Spec.PasswordSecrets { - secret, err := r.k8sClient.GetSecret(ctx, instance.Namespace, secretName) - if err != nil { - return err - } - if secret.GetLabels() == nil { - secret.SetLabels(map[string]string{}) - } - - if secret.Labels["middleware.instance/name"] != instance.Spec.RedisName { - secret.Labels["managed-by"] = "redis-operator" - secret.Labels["middleware.instance/name"] = instance.Spec.RedisName - secret.OwnerReferences = util.BuildOwnerReferences(&instance) - err = r.k8sClient.UpdateSecret(ctx, instance.Namespace, secret) - if err != nil { - return err - } - } - passwords = append(passwords, string(secret.Data["password"])) - password_obj = &user.Password{ - SecretName: secretName, - } - } - - r.logger.Info("redis user do", "redis user name", instance.Name, "type", instance.Spec.Arch) - switch instance.Spec.Arch { - case redis.ClusterArch: - r.logger.Info("cluster", "redis name", instance.Spec.RedisName) - rc, err := r.k8sClient.GetDistributedRedisCluster(ctx, instance.Namespace, instance.Spec.RedisName) - if err != nil { - return err - } - rcm, err := clustermodel.NewRedisCluster(ctx, r.k8sClient, rc, r.logger) - if err != nil { - return err - } - if !rcm.IsReady() { - r.logger.Info("redis instance is not ready", "redis name", instance.Spec.RedisName) - return fmt.Errorf("redis instance is not ready") - } - - configmap, err := r.k8sClient.GetConfigMap(ctx, instance.Namespace, clusterbuilder.GenerateClusterACLConfigMapName(instance.Spec.RedisName)) - if err != nil { - return err - } - rule := instance.Spec.AclRules - if rcm.Version().IsACLSupported() { - rule = AddClusterRule(instance.Spec.AclRules) - } - userObj, err := user.NewUserFromRedisUser(instance.Spec.Username, rule, password_obj) - if err != nil { - return err - } - info, err := json.Marshal(userObj) - if err != nil { - return err - } - configmap.Data[instance.Spec.Username] = string(info) - - for _, node := range rcm.Nodes() { - - _, err := node.SetACLUser(ctx, instance.Spec.Username, passwords, rule) - if err != nil { - r.logger.Error(err, "acl set user failed", "node", node.GetName()) - return err - } - r.logger.Info("acl set user success", "node", node.GetName()) - } - - if err := r.k8sClient.UpdateConfigMap(ctx, instance.Namespace, configmap); err != nil { - return err - } - case redis.SentinelArch: - r.logger.Info("sentinel", "redis name", instance.Spec.RedisName) - rf, err := r.k8sClient.GetRedisFailover(ctx, instance.Namespace, instance.Spec.RedisName) - if err != nil { - return err - } - rfm, err := sentinelmodle.NewRedisFailover(ctx, r.k8sClient, rf, r.logger) - if err != nil { - return err - } - if !rfm.IsReady() { - r.logger.Info("redis instance is not ready", "redis name", instance.Spec.RedisName) - return fmt.Errorf("redis instance is not ready") - } - configmap, err := r.k8sClient.GetConfigMap(ctx, instance.Namespace, sentinelbuilder.GenerateSentinelACLConfigMapName(instance.Spec.RedisName)) - if err != nil { - return err - } - userObj, err := user.NewUserFromRedisUser(instance.Spec.Username, instance.Spec.AclRules, password_obj) - if err != nil { - return err - } - info, err := json.Marshal(userObj) - if err != nil { - return err - } - configmap.Data[instance.Spec.Username] = string(info) - for _, node := range rfm.Nodes() { - _, err := node.SetACLUser(ctx, instance.Spec.Username, passwords, instance.Spec.AclRules) - if err != nil { - r.logger.Error(err, "acl set user failed", "node", node.GetName()) - return err - } - r.logger.Info("acl set user success", "node", node.GetName()) - } - if err := r.k8sClient.UpdateConfigMap(ctx, instance.Namespace, configmap); err != nil { - return err - } - - } - - return nil -} - -// 补充cluster 规则 -func AddClusterRule(rules string) string { - //slice rule - for _, rule := range strings.Split(rules, " ") { - if slices.Contains([]string{"-@all", "-@dangerous", "-@admin", "-@slow"}, rule) { - rules += " " + "+cluster|slots +cluster|nodes +cluster|info +cluster|keyslot +cluster|GETKEYSINSLOT +cluster|COUNTKEYSINSLOT" - return rules - } - } - return rules -} diff --git a/internal/controller/redis/utils/const.go b/internal/controller/redis/utils/const.go deleted file mode 100644 index 0d78c82..0000000 --- a/internal/controller/redis/utils/const.go +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package utils - -const ( - RedisPasswordSecretKey = "password" - - // storage - StorageProvisionerKey = "volume.beta.kubernetes.io/storage-provisioner" - TopoLVMProvisionerKey = "topolvm.cybozu.com" -) diff --git a/internal/controller/redis/utils/name.go b/internal/controller/redis/utils/name.go deleted file mode 100644 index c7dfd7d..0000000 --- a/internal/controller/redis/utils/name.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package utils - -import ( - "fmt" - "time" -) - -const ( - RedisBaseName = "redis" -) - -func GenerateRedisSecretName(instanceName string) string { - return fmt.Sprintf("%s-%s-password-%d", RedisBaseName, instanceName, time.Now().Unix()) -} -func GetRedisFailoverName(instanceName string) string { - return instanceName -} -func GetRedisClusterName(instanceName string) string { - return instanceName -} - -func GetRedisStorageVolumeName(instanceName string) string { - return "redis-data" -} diff --git a/pkg/ops/cluster/actor/actor_clean_resource.go b/internal/ops/cluster/actor/actor_clean_resource.go similarity index 73% rename from pkg/ops/cluster/actor/actor_clean_resource.go rename to internal/ops/cluster/actor/actor_clean_resource.go index 910b979..bef5842 100644 --- a/pkg/ops/cluster/actor/actor_clean_resource.go +++ b/internal/ops/cluster/actor/actor_clean_resource.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,21 +18,30 @@ package actor import ( "context" - "fmt" "time" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + cops "github.com/alauda/redis-operator/internal/ops/cluster" + "github.com/alauda/redis-operator/internal/util" "github.com/alauda/redis-operator/pkg/actor" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/slot" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/Masterminds/semver/v3" "github.com/go-logr/logr" ) var _ actor.Actor = (*actorCleanResource)(nil) +func init() { + actor.Register(core.RedisCluster, NewCleanResourceActor) +} + func NewCleanResourceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { return &actorCleanResource{ client: client, @@ -49,11 +58,16 @@ func (a *actorCleanResource) SupportedCommands() []actor.Command { return []actor.Command{cops.CommandCleanResource} } +func (a *actorCleanResource) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + // Do func (a *actorCleanResource) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", cops.CommandCleanResource.String()) + cluster := val.(types.RedisClusterInstance) cr := cluster.Definition() - logger := a.logger.WithName(cops.CommandCleanResource.String()).WithValues("namespace", cr.Namespace, "name", cr.Name) var ( failedNodeId = []string{} @@ -85,7 +99,7 @@ func (a *actorCleanResource) Do(ctx context.Context, val types.RedisInstance) *a shard := cluster.Shards()[i] for _, node := range shard.Nodes() { if err := node.Setup(ctx, margs...); err != nil { - logger.Error(err, "forget node failed", "node", node.ID()) + logger.Error(err, "forget invalid node failed", "node", node.ID()) } } } @@ -106,21 +120,23 @@ func (a *actorCleanResource) Do(ctx context.Context, val types.RedisInstance) *a name := clusterbuilder.ClusterStatefulSetName(cr.GetName(), shard.Index()) logger.Info("clean resource", "statefulset", name) // NOTE: DeleteStatefulSet with GracePeriodSeconds=0 - if err := a.client.DeleteStatefulSet(ctx, cr.GetNamespace(), name, true); err != nil { - a.logger.Error(err, "delete statefulset failed", "target", fmt.Sprintf("%s/%s", cr.GetNamespace(), name)) + if err := a.client.DeleteStatefulSet(ctx, cr.GetNamespace(), name, client.GracePeriodSeconds(0)); err != nil { + a.logger.Error(err, "delete statefulset failed", "target", util.ObjectKey(cr.GetNamespace(), name)) } + for _, node := range shard.Nodes() { // delete pods - if err := a.client.DeletePod(ctx, node.GetNamespace(), node.GetName(), true); err != nil { - a.logger.Error(err, "force delete pod failed", "target", fmt.Sprintf("%s/%s", cr.GetNamespace(), node.GetName())) + if err := a.client.DeletePod(ctx, node.GetNamespace(), node.GetName(), client.GracePeriodSeconds(0)); err != nil { + a.logger.Error(err, "force delete pod failed", "target", util.ObjectKey(cr.GetNamespace(), node.GetName())) } // delete cm name := "sync-" + node.GetName() if err := a.client.DeleteConfigMap(ctx, node.GetNamespace(), name); err != nil { - a.logger.Error(err, "delete pod related nodes.conf configmap failed", "target", fmt.Sprintf("%s/%s", cr.GetNamespace(), name)) + a.logger.Error(err, "delete pod related nodes.conf configmap failed", "target", util.ObjectKey(cr.GetNamespace(), name)) } } + cluster.SendEventf(corev1.EventTypeNormal, config.EventCleanResource, "cleaned useless shard %s", name) } } return actor.NewResult(cops.CommandRequeue) diff --git a/pkg/ops/cluster/actor/actor_ensure_resource.go b/internal/ops/cluster/actor/actor_ensure_resource.go similarity index 58% rename from pkg/ops/cluster/actor/actor_ensure_resource.go rename to internal/ops/cluster/actor/actor_ensure_resource.go index 5278afa..57e7bde 100644 --- a/pkg/ops/cluster/actor/actor_ensure_resource.go +++ b/internal/ops/cluster/actor/actor_ensure_resource.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,31 +20,36 @@ import ( "context" "fmt" "reflect" - "sort" + "slices" + "strconv" + "strings" "time" - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redisbackup "github.com/alauda/redis-operator/api/redis/v1" + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/ops/cluster" + cops "github.com/alauda/redis-operator/internal/ops/cluster" + "github.com/alauda/redis-operator/internal/util" "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/config" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/ops/cluster" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" - appv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" ) var _ actor.Actor = (*actorEnsureResource)(nil) +func init() { + actor.Register(core.RedisCluster, NewEnsureResourceActor) +} + func NewEnsureResourceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { return &actorEnsureResource{ client: client, @@ -61,21 +66,22 @@ func (a *actorEnsureResource) SupportedCommands() []actor.Command { return []actor.Command{cluster.CommandEnsureResource} } +func (a *actorEnsureResource) Version() *semver.Version { + return semver.MustParse("3.16.0") +} + // Do func (a *actorEnsureResource) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", cops.CommandEnsureResource.String()) cluster := val.(types.RedisClusterInstance) - logger := a.logger.WithName(cops.CommandEnsureResource.String()).WithValues("namespace", cluster.GetNamespace(), "name", cluster.GetName()) if cluster.Definition().Spec.ServiceName == "" { cluster.Definition().Spec.ServiceName = cluster.GetName() } - if (cluster.Definition().Spec.Annotations != nil) && cluster.Definition().Spec.Annotations[config.PAUSE_ANNOTATION_KEY] != "" { + if cluster.Definition().Spec.PodAnnotations[config.PAUSE_ANNOTATION_KEY] != "" { if ret := a.pauseStatefulSet(ctx, cluster, logger); ret != nil { return ret } - if ret := a.pauseBackupCronJob(ctx, cluster, logger); ret != nil { - return ret - } return actor.NewResult(cops.CommandPaused) } @@ -91,10 +97,6 @@ func (a *actorEnsureResource) Do(ctx context.Context, val types.RedisInstance) * return ret } - if ret := a.ensureBackupSchedule(ctx, cluster, logger); ret != nil { - return ret - } - if ret := a.ensureConfigMap(ctx, cluster, logger); ret != nil { return ret } @@ -102,73 +104,36 @@ func (a *actorEnsureResource) Do(ctx context.Context, val types.RedisInstance) * if ret := a.ensureStatefulset(ctx, cluster, logger); ret != nil { return ret } - // if ret := a.ensureRedisUser(ctx, cluster, logger); ret != nil { - // return ret - // } return nil } -/* -func (a *actorEnsureResource) ensureRedisUser(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { - cr := cluster.Definition() - if cluster.Users().GetOpUser() != nil { - redisUser := clusterbuilder.GenerateClusterOperatorsRedisUser(cluster, cluster.Users().GetOpUser().GetPassword().SecretName) - if err := a.client.CreateIfNotExistsRedisUser(ctx, &redisUser); err != nil { - logger.Error(err, "create redis user failed", "target", client.ObjectKeyFromObject(&redisUser)) - return actor.NewResultWithError(cops.CommandRequeue, err) - } - } - if cluster.Users().GetOpUser() != nil { - redisUser := clusterbuilder.GenerateClusterDefaultRedisUser(cr, cluster.Users().GetDefaultUser().GetPassword().SecretName) - if err := a.client.CreateIfNotExistsRedisUser(ctx, &redisUser); err != nil { - logger.Error(err, "create redis user failed", "target", client.ObjectKeyFromObject(&redisUser)) - return actor.NewResultWithError(cops.CommandRequeue, err) - } - } - - return nil -} -*/ - func (a *actorEnsureResource) pauseStatefulSet(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { cr := cluster.Definition() labels := clusterbuilder.GetClusterLabels(cr.Name, nil) stss, err := a.client.ListStatefulSetByLabels(ctx, cr.Namespace, labels) if err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + logger.Error(err, "load statefulsets failed") + return actor.RequeueWithError(err) } if len(stss.Items) == 0 { return nil } - for _, sts := range stss.Items { - if *sts.Spec.Replicas == 0 { + + pausedCount := 0 + for _, item := range stss.Items { + if *item.Spec.Replicas == 0 { continue } + sts := item.DeepCopy() *sts.Spec.Replicas = 0 - if err = a.client.UpdateStatefulSet(ctx, cr.Namespace, &sts); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + if err = a.client.UpdateStatefulSet(ctx, cr.Namespace, sts); err != nil { + return actor.RequeueWithError(err) } + pausedCount += 1 } - return nil -} -func (a *actorEnsureResource) pauseBackupCronJob(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { - cr := cluster.Definition() - selectorLabels := map[string]string{ - "redis.kun/name": cr.Name, - "managed-by": "redis-cluster-operator", - } - jobsRes, err := a.client.ListCronJobs(ctx, cr.GetNamespace(), client.ListOptions{ - LabelSelector: labels.SelectorFromSet(selectorLabels), - }) - if err != nil { - logger.Error(err, "load cronjobs failed") - return actor.NewResultWithError(cops.CommandRequeue, err) - } - for _, val := range jobsRes.Items { - if err := a.client.DeleteCronJob(ctx, cr.GetNamespace(), val.Name); err != nil { - logger.Error(err, "delete cronjob failed", "target", client.ObjectKeyFromObject(&val)) - } + if pausedCount > 0 { + cluster.SendEventf(corev1.EventTypeNormal, config.EventPause, "paused instance statefulsets") } return nil } @@ -184,24 +149,25 @@ func (a *actorEnsureResource) ensureServiceAccount(ctx context.Context, cluster clusterBinding := clusterbuilder.NewClusterRoleBinding(cr) if err := a.client.CreateOrUpdateServiceAccount(ctx, cluster.GetNamespace(), sa); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + logger.Error(err, "create serviceaccount failed", "target", client.ObjectKeyFromObject(sa)) + return actor.RequeueWithError(err) } if err := a.client.CreateOrUpdateRole(ctx, cluster.GetNamespace(), role); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } if err := a.client.CreateOrUpdateRoleBinding(ctx, cluster.GetNamespace(), binding); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } if err := a.client.CreateOrUpdateClusterRole(ctx, clusterRole); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } if oldClusterRb, err := a.client.GetClusterRoleBinding(ctx, clusterBinding.Name); err != nil { if errors.IsNotFound(err) { if err := a.client.CreateClusterRoleBinding(ctx, clusterBinding); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } } else { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } } else { exists := false @@ -213,12 +179,12 @@ func (a *actorEnsureResource) ensureServiceAccount(ctx context.Context, cluster if !exists && len(oldClusterRb.Subjects) > 0 { oldClusterRb.Subjects = append(oldClusterRb.Subjects, rbacv1.Subject{Kind: "ServiceAccount", - Name: util.RedisBackupServiceAccountName, + Name: clusterbuilder.RedisInstanceServiceAccountName, Namespace: cluster.GetNamespace()}, ) err := a.client.CreateOrUpdateClusterRoleBinding(ctx, oldClusterRb) if err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } } } @@ -236,11 +202,11 @@ func (a *actorEnsureResource) ensureConfigMap(ctx context.Context, cluster types if oldCm, err := a.client.GetConfigMap(ctx, cluster.GetNamespace(), cm.GetName()); errors.IsNotFound(err) { if err := a.client.CreateConfigMap(ctx, cluster.GetNamespace(), cm); err != nil { logger.Error(err, "update configmap failed") - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } } else if !reflect.DeepEqual(oldCm.Data, cm.Data) { if err := a.client.UpdateConfigMap(ctx, cluster.GetNamespace(), cm); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } } return nil @@ -255,17 +221,17 @@ func (a *actorEnsureResource) ensureTLS(ctx context.Context, cluster types.Redis oldCert, err := a.client.GetCertificate(ctx, cluster.GetNamespace(), cert.GetName()) if err != nil && !errors.IsNotFound(err) { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } if err := a.client.CreateIfNotExistsCertificate(ctx, cluster.GetNamespace(), cert); err != nil { logger.Error(err, "request for certificate failed") - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } var ( found = false - secretName = clusterbuilder.GetRedisSSLSecretName(cluster.GetName()) + secretName = builder.GetRedisSSLSecretName(cluster.GetName()) ) for i := 0; i < 5; i++ { time.Sleep(time.Second * time.Duration(i)) @@ -289,32 +255,12 @@ func (a *actorEnsureResource) ensureTLS(ctx context.Context, cluster types.Redis // ensureStatefulset func (a *actorEnsureResource) ensureStatefulset(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { cr := cluster.Definition() - isRestoring := func(sts *appv1.StatefulSet) bool { - if cr.Spec.Restore.BackupName != "" { - if sts == nil { - return true - } - if util.GetContainerByName(&sts.Spec.Template.Spec, clusterbuilder.RestoreContainerName) == nil { - return true - } - } - return false - } var ( - err error - backup *redisbackup.RedisClusterBackup // isAllACLSupported is used to make sure 5=>6 upgrade can to failover succeed - isAllACLSupported = true + isAllACLSupported = cluster.Version().IsACLSupported() updated = false ) - if cr.Spec.Restore.BackupName != "" { - if backup, err = a.client.GetRedisClusterBackup(ctx, cr.GetNamespace(), cr.Spec.Restore.BackupName); err != nil { - logger.Error(err, "load redis cluster backup cr failed", - "target", client.ObjectKey{Namespace: cr.GetNamespace(), Name: cr.Spec.Restore.BackupName}) - return actor.NewResultWithError(cops.CommandRequeue, err) - } - } __end__: for _, shard := range cluster.Shards() { @@ -329,12 +275,31 @@ __end__: for i := 0; i < int(cr.Spec.MasterSize); i++ { // statefulset name := clusterbuilder.ClusterStatefulSetName(cr.GetName(), i) - if oldSts, err := a.client.GetStatefulSet(ctx, cr.GetNamespace(), name); errors.IsNotFound(err) { - newSts, err := clusterbuilder.NewStatefulSetForCR(cluster, false, isAllACLSupported, backup, i) - if err != nil { - logger.Error(err, "generate statefulset failed") - return actor.NewResultWithError(cops.CommandAbort, err) + + pdb := clusterbuilder.NewPodDisruptionBudgetForCR(cr, i) + if oldPdb, err := a.client.GetPodDisruptionBudget(ctx, cr.GetNamespace(), pdb.Name); errors.IsNotFound(err) { + if err = a.client.CreatePodDisruptionBudget(ctx, cr.GetNamespace(), pdb); err != nil { + logger.Error(err, "create poddisruptionbudget failed", "target", client.ObjectKeyFromObject(pdb)) + return actor.RequeueWithError(err) + } + } else if err != nil { + logger.Error(err, "get poddisruptionbudget failed", "target", client.ObjectKeyFromObject(pdb)) + return actor.RequeueWithError(err) + } else if !reflect.DeepEqual(oldPdb.Spec, pdb.Spec) { + pdb.ResourceVersion = oldPdb.ResourceVersion + if err = a.client.UpdatePodDisruptionBudget(ctx, cr.GetNamespace(), pdb); err != nil { + logger.Error(err, "update poddisruptionbudget failed", "target", client.ObjectKeyFromObject(pdb)) + return actor.RequeueWithError(err) } + } + + newSts, err := clusterbuilder.NewStatefulSetForCR(cluster, isAllACLSupported, i) + if err != nil { + logger.Error(err, "generate statefulset failed") + return actor.NewResultWithError(cops.CommandAbort, err) + } + + if oldSts, err := a.client.GetStatefulSet(ctx, cr.GetNamespace(), name); errors.IsNotFound(err) { if err := a.client.CreateStatefulSet(ctx, cr.GetNamespace(), newSts); err != nil { logger.Error(err, "create statefulset failed") return actor.NewResultWithError(cops.CommandAbort, err) @@ -342,33 +307,27 @@ __end__: updated = true } else if err != nil { logger.Error(err, "get statefulset failed") - return actor.NewResultWithError(cops.CommandRequeue, err) - } else if newSts, err := clusterbuilder.NewStatefulSetForCR(cluster, isRestoring(oldSts), isAllACLSupported, backup, i); err != nil { - logger.Error(err, "build init statefulset failed") - return actor.NewResultWithError(cops.CommandAbort, err) + return actor.RequeueWithError(err) } else { // overwrite persist fields // keep old affinity for topolvm cases + // TODO: remove in future newSts.Spec.Template.Spec.Affinity = oldSts.Spec.Template.Spec.Affinity + // keep old selector for upgrade if !reflect.DeepEqual(oldSts.Spec.Selector.MatchLabels, newSts.Spec.Selector.MatchLabels) { newSts.Spec.Selector.MatchLabels = oldSts.Spec.Selector.MatchLabels } - // keep labels for pvc - for i, npvc := range newSts.Spec.VolumeClaimTemplates { - if oldPvc := util.GetVolumeClaimTemplatesByName(oldSts.Spec.VolumeClaimTemplates, npvc.Name); oldPvc != nil { - newSts.Spec.VolumeClaimTemplates[i].Labels = oldPvc.Labels - } - } + // keep old pvc + newSts.Spec.VolumeClaimTemplates = oldSts.Spec.VolumeClaimTemplates // merge restart annotations, if statefulset is more new, not restart statefulset - newSts.Spec.Template.Annotations = MergeAnnotations(newSts.Spec.Template.Annotations, - oldSts.Spec.Template.Annotations) + newSts.Spec.Template.Annotations = MergeAnnotations(newSts.Spec.Template.Annotations, oldSts.Spec.Template.Annotations) - if clusterbuilder.IsRedisClusterStatefulsetChanged(newSts, oldSts, logger) { + if clusterbuilder.IsStatefulsetChanged(newSts, oldSts, logger) { if err := a.client.UpdateStatefulSet(ctx, cr.GetNamespace(), newSts); err != nil { logger.Error(err, "update statefulset failed", "target", client.ObjectKeyFromObject(newSts)) - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } updated = true } @@ -395,69 +354,66 @@ func (a *actorEnsureResource) ensureService(ctx context.Context, cluster types.R } } - // common service svc := clusterbuilder.NewServiceForCR(cr) if err := a.client.CreateIfNotExistsService(ctx, cr.GetNamespace(), svc); err != nil { logger.Error(err, "create service failed", "target", client.ObjectKeyFromObject(svc)) - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } - if cr.Spec.Expose.EnableNodePort { - // pod svc - if cr.Spec.Expose.DataStorageNodePortSequence != "" { - logger.Info("EnsureRedisNodePortService DataStorageNodePortSequence", "Namespace", cr.Namespace, "Name", cr.Name) - if err := a.ensureRedisNodePortService(ctx, cluster, logger); err != nil { - return err - } + if ret := a.cleanUselessService(ctx, cluster, logger); ret != nil { + return ret + } + switch cr.Spec.Expose.ServiceType { + case corev1.ServiceTypeNodePort: + if err := a.ensureRedisNodePortService(ctx, cluster, logger); err != nil { + return err } - nodePortSvc := clusterbuilder.NewNodePortServiceForCR(cr) + + nodePortSvc := clusterbuilder.NewServiceWithType(cr, corev1.ServiceTypeNodePort, cr.Spec.Expose.AccessPort) if err := a.client.CreateIfNotExistsService(ctx, cr.GetNamespace(), nodePortSvc); err != nil { logger.Error(err, "create nodeport service failed", "target", client.ObjectKeyFromObject(nodePortSvc)) - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) + } + case corev1.ServiceTypeLoadBalancer: + if ret := a.ensureRedisPodService(ctx, cluster, logger); ret != nil { + return ret + } + svc := clusterbuilder.NewServiceWithType(cr, corev1.ServiceTypeLoadBalancer, 0) + if err := a.client.CreateIfNotExistsService(ctx, cr.GetNamespace(), svc); err != nil { + logger.Error(err, "create service failed", "target", client.ObjectKeyFromObject(svc)) + return actor.RequeueWithError(err) } } return nil } func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { - cr := cluster.Definition() + if cluster.Definition().Spec.Expose.ServiceType != corev1.ServiceTypeNodePort { + return nil + } + + if cluster.Definition().Spec.Expose.NodePortSequence == "" { + return a.ensureRedisPodService(ctx, cluster, logger) + } - configedPorts, err := util.ParsePortSequence(cr.Spec.Expose.DataStorageNodePortSequence) + cr := cluster.Definition() + configedPorts, err := helper.ParseSequencePorts(cr.Spec.Expose.NodePortSequence) if err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } getClientPort := func(svc *corev1.Service) int32 { - if port := getServicePortByName(svc, "client"); port != nil { + if port := util.GetServicePortByName(svc, "client"); port != nil { return port.NodePort } return 0 } getGossipPort := func(svc *corev1.Service) int32 { - if port := getServicePortByName(svc, "gossip"); port != nil { + if port := util.GetServicePortByName(svc, "gossip"); port != nil { return port.NodePort } return 0 } - fetchAllPodBindedServices := func() ([]corev1.Service, *actor.ActorResult) { - var ( - services []corev1.Service - ) - - labels := clusterbuilder.GetClusterLabels(cr.Name, nil) - if svcRes, err := a.client.GetServiceByLabels(ctx, cr.Namespace, labels); err != nil { - return nil, actor.NewResultWithError(cops.CommandRequeue, err) - } else { - // ignore services without pod selector - for _, svc := range svcRes.Items { - if svc.Spec.Selector["statefulset.kubernetes.io/pod-name"] != "" { - services = append(services, svc) - } - } - } - return services, nil - } - serviceNameRange := map[string]struct{}{} for shard := 0; shard < int(cr.Spec.MasterSize); shard++ { for replica := 0; replica < int(cr.Spec.ClusterReplicas)+1; replica++ { @@ -470,7 +426,7 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl clusterNodePortSvc, err := a.client.GetService(ctx, cr.GetNamespace(), clusterbuilder.RedisNodePortSvcName(cr.Name)) if err != nil && !errors.IsNotFound(err) { a.logger.Error(err, "get cluster nodeport service failed", "target", clusterbuilder.RedisNodePortSvcName(cr.Name)) - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } // the whole process is divided into three steps: @@ -479,7 +435,7 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl // 3. update existing service and restart pod (only one pod is restarted at a same time for each shard) // 4. check again if all gossip port is added - services, ret := fetchAllPodBindedServices() + services, ret := a.fetchAllPodBindedServices(ctx, cr.Namespace, labels) if ret != nil { return ret } @@ -490,28 +446,29 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl // NOTE: only delete service whose pod is not found // let statefulset auto scale up/down for pods for _, svc := range services { - if _, exists := serviceNameRange[svc.Name]; !exists || !isPortInRange(configedPorts, getClientPort(&svc)) { - pod, err := a.client.GetPod(ctx, svc.Namespace, svc.Name) + svc := svc.DeepCopy() + if _, exists := serviceNameRange[svc.Name]; !exists || !slices.Contains(configedPorts, getClientPort(svc)) { + _, err := a.client.GetPod(ctx, svc.Namespace, svc.Name) if errors.IsNotFound(err) { - logger.Info("release nodeport service", "service", svc.Name, "port", getClientPort(&svc)) + logger.Info("release nodeport service", "service", svc.Name, "port", getClientPort(svc)) if err = a.client.DeleteService(ctx, svc.Namespace, svc.Name); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } } else if err != nil { - logger.Error(err, "get pods failed", "target", client.ObjectKeyFromObject(pod)) - return actor.NewResultWithError(cops.CommandRequeue, err) + logger.Error(err, "get pods failed", "target", client.ObjectKeyFromObject(svc)) + return actor.RequeueWithError(err) } } } - if clusterNodePortSvc != nil && isPortInRange(configedPorts, clusterNodePortSvc.Spec.Ports[0].NodePort) { + if clusterNodePortSvc != nil && slices.Contains(configedPorts, clusterNodePortSvc.Spec.Ports[0].NodePort) { // delete cluster nodeport service if err := a.client.DeleteService(ctx, cr.GetNamespace(), clusterNodePortSvc.Name); err != nil { a.logger.Error(err, "delete service failed", "target", client.ObjectKeyFromObject(clusterNodePortSvc)) - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } } - if services, ret = fetchAllPodBindedServices(); ret != nil { + if services, ret = a.fetchAllPodBindedServices(ctx, cr.Namespace, labels); ret != nil { return ret } @@ -523,14 +480,15 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl needUpdateGossipServices []*corev1.Service ) for _, svc := range services { - bindedNodeports = append(bindedNodeports, getClientPort(&svc), getGossipPort(&svc)) + svc := svc.DeepCopy() + bindedNodeports = append(bindedNodeports, getClientPort(svc), getGossipPort(svc)) } if clusterNodePortSvc != nil && len(clusterNodePortSvc.Spec.Ports) > 0 { bindedNodeports = append(bindedNodeports, clusterNodePortSvc.Spec.Ports[0].NodePort) } // filter used ports for _, port := range configedPorts { - if !isPortInRange(bindedNodeports, port) { + if !slices.Contains(bindedNodeports, port) { newPorts = append(newPorts, port) } } @@ -549,8 +507,9 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl return actor.NewResultWithValue(cops.CommandRequeue, err) } newPorts = newPorts[1:] + continue } else if err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } // check old service for compability @@ -561,9 +520,9 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl return actor.NewResultWithValue(cops.CommandRequeue, err) } } - if isPortInRange(configedPorts, getGossipPort(oldService)) { + if slices.Contains(configedPorts, getGossipPort(oldService)) { needUpdateGossipServices = append(needUpdateGossipServices, oldService) - } else if port := getClientPort(oldService); port != 0 && !isPortInRange(configedPorts, port) { + } else if port := getClientPort(oldService); port != 0 && !slices.Contains(configedPorts, port) { needUpdateServices = append(needUpdateServices, oldService) } } @@ -572,7 +531,7 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl // 3. update existing service and restart pod (only one pod is restarted at a same time for each shard) if len(needUpdateServices) > 0 && len(newPorts) > 0 { port, svc := newPorts[0], needUpdateServices[0] - if sp := getServicePortByName(svc, "client"); sp != nil { + if sp := util.GetServicePortByName(svc, "client"); sp != nil { sp.NodePort = port } @@ -584,9 +543,9 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl a.logger.Error(err, "update nodeport service failed", "target", client.ObjectKeyFromObject(svc), "port", port) return actor.NewResultWithValue(cops.CommandRequeue, err) } - if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector["statefulset.kubernetes.io/pod-name"]); pod != nil { + if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector[builder.PodNameLabelKey]); pod != nil { if err := a.client.DeletePod(ctx, cr.Namespace, pod.Name); err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } return actor.NewResult(cops.CommandRequeue) } @@ -610,10 +569,10 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl a.logger.Error(err, "update nodeport service failed", "target", client.ObjectKeyFromObject(svc)) return actor.NewResultWithValue(cops.CommandRequeue, err) } - if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector["statefulset.kubernetes.io/pod-name"]); pod != nil { + if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector[builder.PodNameLabelKey]); pod != nil { if err := a.client.DeletePod(ctx, cr.Namespace, pod.Name); err != nil { a.logger.Error(err, "delete pod failed", "target", client.ObjectKeyFromObject(pod)) - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } return actor.NewResult(cops.CommandRequeue) } @@ -626,7 +585,7 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl if svc, err := a.client.GetService(ctx, cr.Namespace, serviceName); errors.IsNotFound(err) { continue } else if err != nil { - a.logger.Error(err, "get service failed", "target", client.ObjectKeyFromObject(svc)) + a.logger.Error(err, "get service failed", "target", util.ObjectKey(cr.Namespace, serviceName)) } else if port := getGossipPort(svc); port == 0 && len(svc.Spec.Ports) == 1 { // update gossip service svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{Name: "gossip", Port: 16379}) @@ -634,10 +593,10 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl a.logger.Error(err, "update nodeport service failed", "target", client.ObjectKeyFromObject(svc)) return actor.NewResultWithValue(cops.CommandRequeue, err) } - if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector["statefulset.kubernetes.io/pod-name"]); pod != nil { + if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector[builder.PodNameLabelKey]); pod != nil { if err := a.client.DeletePod(ctx, cr.Namespace, pod.Name); err != nil { a.logger.Error(err, "delete pod failed", "target", client.ObjectKeyFromObject(pod)) - return actor.NewResultWithError(cops.CommandRequeue, err) + return actor.RequeueWithError(err) } return actor.NewResult(cops.CommandRequeue) } @@ -647,101 +606,86 @@ func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, cl return nil } -// ensureBackupSchedule -func (a *actorEnsureResource) ensureBackupSchedule(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { +func (a *actorEnsureResource) ensureRedisPodService(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { cr := cluster.Definition() + labels := clusterbuilder.GetClusterLabels(cr.Name, nil) - for _, schedule := range cr.Spec.Backup.Schedule { - ls := map[string]string{ - "redis.kun/name": cr.Name, - "redis.kun/scheduleName": schedule.Name, - } - res, err := a.client.ListRedisClusterBackups(ctx, cr.GetNamespace(), client.ListOptions{ - LabelSelector: labels.SelectorFromSet(ls), - }) - if err != nil { - logger.Error(err, "load backups failed", "target", client.ObjectKeyFromObject(cr)) - return actor.NewResultWithError(cops.CommandRequeue, err) - } - sort.SliceStable(res.Items, func(i, j int) bool { - return res.Items[i].GetCreationTimestamp().After(res.Items[j].GetCreationTimestamp().Time) - }) - for i := len(res.Items) - 1; i >= int(schedule.Keep); i-- { - item := res.Items[i] - if err := a.client.DeleteRedisClusterBackup(ctx, item.GetNamespace(), item.GetName()); err != nil { - logger.V(2).Error(err, "clean old backup failed", "target", client.ObjectKeyFromObject(&item)) + for shard := 0; shard < int(cr.Spec.MasterSize); shard++ { + for replica := 0; replica < int(cr.Spec.ClusterReplicas)+1; replica++ { + serviceName := clusterbuilder.ClusterNodeSvcName(cr.Name, shard, replica) + newSvc := clusterbuilder.NewPodService(cr, serviceName, cr.Spec.Expose.ServiceType, labels, cr.Spec.Expose.Annotations) + if svc, err := a.client.GetService(ctx, cr.Namespace, serviceName); errors.IsNotFound(err) { + if err = a.client.CreateService(ctx, cr.Namespace, newSvc); err != nil { + a.logger.Error(err, "create service failed", "target", client.ObjectKeyFromObject(newSvc)) + return actor.RequeueWithError(err) + } + } else if err != nil { + a.logger.Error(err, "get service failed", "target", client.ObjectKeyFromObject(newSvc)) + return actor.NewResult(cops.CommandRequeue) + } else if svc.Spec.Type != newSvc.Spec.Type || + !reflect.DeepEqual(svc.Spec.Selector, newSvc.Spec.Selector) || + !reflect.DeepEqual(svc.Labels, newSvc.Labels) || + !reflect.DeepEqual(svc.Annotations, newSvc.Annotations) { + if err = a.client.UpdateService(ctx, newSvc.Namespace, newSvc); err != nil { + a.logger.Error(err, "update service failed", "target", client.ObjectKeyFromObject(newSvc)) + return actor.RequeueWithError(err) + } } } } + return nil +} - // check backup schedule - // ref: https://jira.alauda.cn/browse/MIDDLEWARE-21009 - deprecatedSelectorLabels := map[string]string{ - "redis.kun/name": cr.Name, - "managed-by": "redis-cluster-operator", +func (a *actorEnsureResource) cleanUselessService(ctx context.Context, cluster types.RedisClusterInstance, logger logr.Logger) *actor.ActorResult { + cr := cluster.Definition() + if cr.Spec.Expose.ServiceType != corev1.ServiceTypeLoadBalancer && cr.Spec.Expose.ServiceType != corev1.ServiceTypeNodePort { + return nil } - deprecatedJobsRes, err := a.client.ListCronJobs(ctx, cr.GetNamespace(), client.ListOptions{ - LabelSelector: labels.SelectorFromSet(deprecatedSelectorLabels), - }) - if err != nil { - return actor.NewResultWithError(cops.CommandRequeue, err) + + labels := clusterbuilder.GetClusterLabels(cr.Name, nil) + services, ret := a.fetchAllPodBindedServices(ctx, cr.Namespace, labels) + if ret != nil { + return ret } - for _, v := range deprecatedJobsRes.Items { - logger.Info("delete deprecated cronjob", "target", client.ObjectKeyFromObject(&v)) - err := a.client.DeleteCronJob(ctx, cr.GetNamespace(), v.Name) + for _, item := range services { + svc := item.DeepCopy() + shard, index, err := clusterbuilder.ParsePodShardAndIndex(svc.Name) if err != nil { - logger.Error(err, "delete cronjob failed", "target", client.ObjectKeyFromObject(&v)) + logger.Error(err, "parse svc name failed", "target", client.ObjectKeyFromObject(svc)) + continue } - } - selectorLabels := map[string]string{ - "redisclusterbackups.redis.middleware.alauda.io/instanceName": cr.Name, - "managed-by": "redis-cluster-operator", - } - jobsRes, err := a.client.ListCronJobs(ctx, cr.GetNamespace(), client.ListOptions{ - LabelSelector: labels.SelectorFromSet(selectorLabels), - }) - if err != nil { - logger.Error(err, "load cronjobs failed") - return actor.NewResultWithError(cops.CommandRequeue, err) - } - jobsMap := map[string]*batchv1.CronJob{} - for _, item := range jobsRes.Items { - jobsMap[item.Name] = &item - } - for _, sched := range cr.Spec.Backup.Schedule { - sc := v1alpha1.Schedule{ - Name: sched.Name, - Schedule: sched.Schedule, - Keep: sched.Keep, - KeepAfterDeletion: sched.KeepAfterDeletion, - Storage: v1alpha1.RedisBackupStorage(sched.Storage), - Target: sched.Target, - } - job := clusterbuilder.NewRedisClusterBackupCronJobFromCR(sc, cr) - if oldJob := jobsMap[job.GetName()]; oldJob != nil { - if clusterbuilder.IsCronJobChanged(job, oldJob, logger) { - if err := a.client.UpdateCronJob(ctx, oldJob.GetNamespace(), job); err != nil { - logger.Error(err, "update cronjob failed", "target", client.ObjectKeyFromObject(oldJob)) - return actor.NewResultWithError(cops.CommandRequeue, err) + if shard+1 > int(cr.Spec.MasterSize) || index > int(cr.Spec.ClusterReplicas) { + _, err := a.client.GetPod(ctx, svc.Namespace, svc.Name) + if errors.IsNotFound(err) { + if err = a.client.DeleteService(ctx, svc.Namespace, svc.Name); err != nil { + return actor.RequeueWithError(err) } + } else if err != nil { + logger.Error(err, "get pods failed", "target", client.ObjectKeyFromObject(svc)) + return actor.RequeueWithError(err) } - jobsMap[job.GetName()] = nil - } else if err := a.client.CreateCronJob(ctx, cr.GetNamespace(), job); err != nil { - logger.Error(err, "create redisbackup cronjob failed", "target", client.ObjectKeyFromObject(job)) - return actor.NewResultWithError(cops.CommandRequeue, err) } } + return nil +} - for name, val := range jobsMap { - if val == nil { - continue - } - if err := a.client.DeleteCronJob(ctx, cr.GetNamespace(), name); err != nil { - logger.Error(err, "delete cronjob failed", "target", client.ObjectKeyFromObject(val)) +func (a *actorEnsureResource) fetchAllPodBindedServices(ctx context.Context, namespace string, labels map[string]string) ([]corev1.Service, *actor.ActorResult) { + var ( + services []corev1.Service + ) + + if svcRes, err := a.client.GetServiceByLabels(ctx, namespace, labels); err != nil { + return nil, actor.RequeueWithError(err) + } else { + // ignore services without pod selector + for _, svc := range svcRes.Items { + if svc.Spec.Selector[builder.PodNameLabelKey] != "" { + services = append(services, svc) + } } } - return nil + return services, nil } // NOTE: removed proxy ensure, remove proxy ensure to rds @@ -779,23 +723,16 @@ func MergeAnnotations(t, s map[string]string) map[string]string { return t } -func getServicePortByName(svc *corev1.Service, name string) *corev1.ServicePort { - for i := range svc.Spec.Ports { - if svc.Spec.Ports[i].Name == name { - return &svc.Spec.Ports[i] - } +func parsePodShardAndIndex(name string) (shard int, index int, err error) { + fields := strings.Split(name, "-") + if len(fields) < 3 { + return -1, -1, fmt.Errorf("invalid pod name %s", name) } - return nil -} - -func isPortInRange(ports []int32, p int32) bool { - if p == 0 { - return false + if index, err = strconv.Atoi(fields[len(fields)-1]); err != nil { + return -1, -1, fmt.Errorf("invalid pod name %s", name) } - for _, port := range ports { - if p == port { - return true - } + if shard, err = strconv.Atoi(fields[len(fields)-2]); err != nil { + return -1, -1, fmt.Errorf("invalid pod name %s", name) } - return false + return shard, index, nil } diff --git a/internal/ops/cluster/actor/actor_ensure_resource_test.go b/internal/ops/cluster/actor/actor_ensure_resource_test.go new file mode 100644 index 0000000..8b0402c --- /dev/null +++ b/internal/ops/cluster/actor/actor_ensure_resource_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import "testing" + +func Test_parsePodShardAndIndex(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + wantShard int + wantIndex int + wantErr bool + }{ + { + name: "name ok", + args: args{name: "drc-redis-1-1"}, + wantShard: 1, + wantIndex: 1, + wantErr: false, + }, + { + name: "name ok", + args: args{name: "drc----redis-0-0"}, + wantShard: 0, + wantIndex: 0, + wantErr: false, + }, + { + name: "name error", + args: args{name: "drc-redis-1"}, + wantShard: -1, + wantIndex: -1, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotShard, gotIndex, err := parsePodShardAndIndex(tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("%s parsePodShardAndIndex() error = %v, wantErr %v", tt.name, err, tt.wantErr) + return + } + if gotShard != tt.wantShard { + t.Errorf("%s parsePodShardAndIndex() gotShard = %v, want %v", tt.name, gotShard, tt.wantShard) + } + if gotIndex != tt.wantIndex { + t.Errorf("%s parsePodShardAndIndex() gotIndex = %v, want %v", tt.name, gotIndex, tt.wantIndex) + } + }) + } +} diff --git a/internal/ops/cluster/actor/actor_ensure_slots.go b/internal/ops/cluster/actor/actor_ensure_slots.go new file mode 100644 index 0000000..6fc8927 --- /dev/null +++ b/internal/ops/cluster/actor/actor_ensure_slots.go @@ -0,0 +1,311 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "fmt" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + cops "github.com/alauda/redis-operator/internal/ops/cluster" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/slot" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" +) + +var _ actor.Actor = (*actorEnsureSlots)(nil) + +func init() { + actor.Register(core.RedisCluster, NewEnsureSlotsActor) +} + +func NewEnsureSlotsActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorEnsureSlots{ + client: client, + logger: logger, + } +} + +type actorEnsureSlots struct { + client kubernetes.ClientSet + + logger logr.Logger +} + +// SupportedCommands +func (a *actorEnsureSlots) SupportedCommands() []actor.Command { + return []actor.Command{cops.CommandEnsureSlots} +} + +func (a *actorEnsureSlots) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + +// Do +// 该 Actor 处理槽分配与槽丢失的情况, 槽的迁移由 Rebanalce + sidecar 处理 +// 处理逻辑如下: +// 判断是否有 master 节点存在,如果无 master 节点,则 failover +// 槽的分配和迁移只根据 cr.status.shards 中预分配的信息来,如果发现某个槽丢失了,以下逻辑会自动添加回来 +// 特殊情况: +// +// 如果发现某个槽被意外移到了其他节点,该槽不会被移动回来,operator 不会处理这个情况,并保留这个状态。在下一个 Reconcile 中,operator 会在cluster inservice 时,刷新 cr.status.shards 信息 +func (a *actorEnsureSlots) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + cluster := val.(types.RedisClusterInstance) + cr := cluster.Definition() + logger := val.Logger().WithValues("actor", cops.CommandEnsureSlots.String()) + + // force refresh the cluster + if err := cluster.Refresh(ctx); err != nil { + logger.Error(err, "refresh cluster info failed") + return actor.NewResultWithError(cops.CommandRequeue, err) + } + if len(cluster.Shards()) == 0 { + return actor.NewResult(cops.CommandEnsureResource) + } + + // check is slots fullfilled + var ( + allSlots = slot.NewSlots() + shardsSlots = map[int]types.RedisClusterShard{} + ) + for i, shard := range cluster.Shards() { + if i != shard.Index() { + return actor.NewResult(cops.CommandEnsureResource) + } + allSlots = allSlots.Union(shard.Slots()) + shardsSlots[shard.Index()] = shard + } + if allSlots.IsFullfilled() { + return nil + } + + var ( + failedShards []types.RedisClusterShard + importingSlotTarget = map[int]types.RedisClusterShard{} + ) + + // Only masters will slots can vote for the slave to promote to be a master + for _, statusShard := range cr.Status.Shards { + var shard types.RedisClusterShard + for _, cs := range cluster.Shards() { + if cs.Index() == int(statusShard.Index) { + shard = cs + break + } + } + for _, status := range statusShard.Slots { + if status.Status == slot.SlotImporting.String() { + imslots, _ := slot.LoadSlots(status.Slots) + for _, slot := range imslots.Slots() { + importingSlotTarget[slot] = shard + } + } + } + } + + // check shard master info + // NOTE: here not check the case when the last statefulset is removed + for i := 0; i < len(cluster.Shards()); i++ { + shard := cluster.Shards()[i] + if shard.Index() != i { + // some shard missing, fix them first. this should not happen here + return actor.NewResult(cops.CommandEnsureResource) + } + + // check if master exists + if shard.Master() == nil { + if len(shard.Replicas()) == 0 { + // pod missing + return actor.NewResult(cops.CommandHealPod) + } + failedShards = append(failedShards, shard) + } + } + + if len(failedShards) > 0 { + for _, shard := range failedShards { + if err := shard.Refresh(ctx); err != nil { + logger.Error(err, "refresh shard info failed", "shard", shard.GetName()) + continue + } + if shard.Master() != nil { + continue + } + + for _, node := range shard.Replicas() { + if !node.IsReady() && node.Role() != core.RedisRoleReplica { + continue + } + + // disable takeover when shard in importing or migrating + if a.doFailover(ctx, node, 10, !shard.IsImporting() && !shard.IsMigrating(), logger) == nil { + cluster.SendEventf(corev1.EventTypeWarning, config.EventFailover, "healed shard %s with new master %s", shard.GetName(), node.GetName()) + if err := shard.Refresh(ctx); err != nil { + logger.Error(err, "refresh shard info failed", "shard", shard.GetName()) + return actor.NewResultWithError(cops.CommandRequeue, err) + } else if shard.Master() == nil { + logger.Error(fmt.Errorf("failover failed"), "no master found after ENSURE failover", "shard", shard.GetName()) + return actor.NewResult(cops.CommandRequeue) + } + } + break + } + // continue do failover with other shards + } + return actor.NewResult(cops.CommandRequeue) + } + + needRefresh := false + for _, shardStatus := range cr.Status.Shards { + assignedSlots := slot.NewSlots() + for _, status := range shardStatus.Slots { + switch status.Status { + case slot.SlotAssigned.String(): + _ = assignedSlots.Set(status.Slots, slot.SlotAssigned) + case slot.SlotMigrating.String(): + // if the slot is migrating, the operator will not assgin it back + _ = assignedSlots.Set(status.Slots, slot.SlotUnassigned) + } + } + shard := shardsSlots[int(shardStatus.Index)] + shardSlots := shard.Slots() + node := shard.Master() + if err := node.Refresh(ctx); err != nil { + logger.Error(err, "refresh node failed") + return actor.NewResultWithError(cops.CommandRequeue, err) + } + if node.Role() != core.RedisRoleMaster { + continue + } + + missingSlots := assignedSlots.Sub(shardSlots).Sub(allSlots) + missingSlotsIndex := missingSlots.Slots() + logger.Info("missing slots", "shardIndex", shardStatus.Index, "slots", shardStatus.Slots) + for _, slotIndex := range missingSlotsIndex { + if importShard := importingSlotTarget[slotIndex]; importShard != nil && importShard.Master() != nil && importShard.Master().ID() != "" { + // set the slot to importing + args := []interface{}{"CLUSTER", "SETSLOT", slotIndex, "NODE", shard.Master().ID()} + if err := importShard.Master().Setup(ctx, args); err == nil { + _ = missingSlots.Set(slotIndex, slot.SlotUnassigned) + for _, shard := range cluster.Shards() { + if shard.Index() == importShard.Index() { + continue + } + if shard.Master() != nil { + if err := shard.Master().Setup(ctx, args); err != nil { + logger.Error(err, "set slot assigned failed", "shard", shard.GetName(), "node", node.GetName(), "slot", slotIndex) + } + } + } + logger.Info("set slot assignment fixed", "shard", shard.GetName(), "node", node.GetName(), "slot", slotIndex) + } else { + logger.Error(err, "set slot assignment failed", "shard", shard.GetName(), "slot", slotIndex) + } + } + } + + missingSlotsIndex = missingSlots.Slots() + if len(missingSlotsIndex) > 0 { + if len(missingSlotsIndex) < shardSlots.Count(slot.SlotAssigned) { + logger.Info("WARNING: as if the slots have been moved unexpectedly, this may case the cluster run in an unbalance state") + } + var ( + args = []interface{}{"CLUSTER", "ADDSLOTS"} + assginedSlots = slot.NewSlots() + ) + for _, s := range missingSlotsIndex { + args = append(args, s) + _ = assginedSlots.Set(s, slot.SlotAssigned) + } + if err := node.Setup(ctx, args); err != nil { + logger.Error(err, "assign slots to shard failed", "shard", shard.GetName()) + return actor.NewResultWithError(cops.CommandRequeue, err) + } + cluster.SendEventf(corev1.EventTypeNormal, config.EventAssignSlots, "setup/fix missing slots %v to node %s", assginedSlots.String(), node.GetName()) + needRefresh = true + } + } + + if needRefresh { + // force refresh the cluster + if err := cluster.Refresh(ctx); err != nil { + logger.Error(err, "refresh cluster info failed") + return actor.NewResultWithError(cops.CommandRequeue, err) + } + } + return nil +} + +func (a *actorEnsureSlots) doFailover(ctx context.Context, node redis.RedisNode, retry int, ensure bool, logger logr.Logger) error { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + args := []interface{}{"CLUSTER", "FAILOVER", "FORCE"} + for i := 0; i < retry+1; i++ { + logger.Info("do shard failover", "node", node.GetName(), "action", args[2]) + if err := node.Setup(ctx, args); err != nil { + logger.Error(err, "do failover failed", "node", node.GetName()) + return err + } + + for j := 0; j < 3; j++ { + time.Sleep(time.Second * 2) + if err := node.Refresh(ctx); err != nil { + logger.Error(err, "refresh node info failed") + return err + } + if node.Role() == core.RedisRoleMaster || (node.Role() == core.RedisRoleReplica && node.IsMasterLinkUp()) { + return nil + } + } + } + + if ensure { + if err := node.Refresh(ctx); err != nil { + logger.Error(err, "refresh node info failed") + return err + } + + args[2] = "TAKEOVER" + logger.Info("do shard failover", "node", node.GetName(), "action", args[2]) + if err := node.Setup(ctx, args); err != nil { + logger.Error(err, "do failover failed", "node", node.GetName()) + return err + } + + for j := 0; j < 3; j++ { + time.Sleep(time.Second * 2) + if err := node.Refresh(ctx); err != nil { + logger.Error(err, "refresh node info failed") + return err + } + if node.Role() == core.RedisRoleMaster || (node.Role() == core.RedisRoleReplica && node.IsMasterLinkUp()) { + return nil + } + } + return fmt.Errorf("takeover failver failed") + } else { + return fmt.Errorf("force failover failed") + } +} diff --git a/internal/ops/cluster/actor/actor_heal_pod.go b/internal/ops/cluster/actor/actor_heal_pod.go new file mode 100644 index 0000000..2ea563c --- /dev/null +++ b/internal/ops/cluster/actor/actor_heal_pod.go @@ -0,0 +1,162 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/ops/cluster" + cops "github.com/alauda/redis-operator/internal/ops/cluster" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" +) + +var _ actor.Actor = (*actorHealPod)(nil) + +func init() { + actor.Register(core.RedisCluster, NewHealPodActor) +} + +func NewHealPodActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorHealPod{ + client: client, + logger: logger, + } +} + +type actorHealPod struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorHealPod) SupportedCommands() []actor.Command { + return []actor.Command{cluster.CommandHealPod} +} + +func (a *actorHealPod) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + +// Do +func (a *actorHealPod) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", cops.CommandHealPod.String()) + + // clean terminating pods + var ( + cluster = val.(types.RedisClusterInstance) + now = time.Now() + ) + pods, err := cluster.RawNodes(ctx) + if err != nil { + logger.Error(err, "get pods failed") + return actor.RequeueWithError(err) + } + + for _, pod := range pods { + timestamp := pod.GetDeletionTimestamp() + if timestamp == nil { + continue + } + grace := time.Second * 30 + if val := pod.GetDeletionGracePeriodSeconds(); val != nil { + grace = time.Duration(*val) * time.Second + } + if now.Sub(timestamp.Time) <= grace { + continue + } + + objKey := client.ObjectKey{Namespace: pod.GetNamespace(), Name: pod.GetName()} + logger.V(2).Info("for delete pod", "name", pod.GetName()) + // force delete the terminating pods + if err := a.client.DeletePod(ctx, cluster.GetNamespace(), pod.GetName(), client.GracePeriodSeconds(0)); err != nil { + logger.Error(err, "force delete pod failed", "target", objKey) + } else { + cluster.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, "force delete blocked terminating pod %s", objKey.Name) + + logger.Info("force delete blocked terminating pod", "target", objKey) + return actor.Requeue() + } + } + + if typ := cluster.Definition().Spec.Expose.ServiceType; typ == corev1.ServiceTypeNodePort || + typ == corev1.ServiceTypeLoadBalancer { + for _, node := range cluster.Nodes() { + if !node.IsReady() { + continue + } + announceIP := node.DefaultIP().String() + announcePort := node.Port() + + svc, err := a.client.GetService(ctx, cluster.GetNamespace(), node.GetName()) + if errors.IsNotFound(err) { + logger.Info("service not found", "name", node.GetName()) + return actor.NewResult(cops.CommandEnsureResource) + } else if err != nil { + logger.Error(err, "get service failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } + if typ == corev1.ServiceTypeNodePort { + port := util.GetServicePortByName(svc, "client") + if port != nil { + if int(port.NodePort) != announcePort { + if err := a.client.DeletePod(ctx, cluster.GetNamespace(), node.GetName()); err != nil { + logger.Error(err, "delete pod failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } else { + cluster.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, + "force delete pod with inconsist annotation %s", node.GetName()) + return actor.Requeue() + } + } + } else { + logger.Error(fmt.Errorf("service port not found"), "service port not found", "name", node.GetName(), "port", "client") + } + } else if typ == corev1.ServiceTypeLoadBalancer { + if index := slices.IndexFunc(svc.Status.LoadBalancer.Ingress, func(ing corev1.LoadBalancerIngress) bool { + return ing.IP == announceIP || ing.Hostname == announceIP + }); index < 0 { + if err := a.client.DeletePod(ctx, cluster.GetNamespace(), node.GetName()); err != nil { + logger.Error(err, "delete pod failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } else { + cluster.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, + "force delete pod with inconsist annotation %s", node.GetName()) + return actor.Requeue() + } + } + } + } + } + + if fullfilled, _ := cluster.IsResourceFullfilled(ctx); !fullfilled { + return actor.NewResult(cops.CommandEnsureResource) + } + return nil +} diff --git a/pkg/ops/cluster/actor/actor_join_node.go b/internal/ops/cluster/actor/actor_join_node.go similarity index 70% rename from pkg/ops/cluster/actor/actor_join_node.go rename to internal/ops/cluster/actor/actor_join_node.go index 6983081..1c22b9e 100644 --- a/pkg/ops/cluster/actor/actor_join_node.go +++ b/internal/ops/cluster/actor/actor_join_node.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -22,18 +22,24 @@ import ( "strings" "time" + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/ops/cluster" + cops "github.com/alauda/redis-operator/internal/ops/cluster" "github.com/alauda/redis-operator/pkg/actor" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/ops/cluster" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" "github.com/go-logr/logr" ) var _ actor.Actor = (*actorJoinNode)(nil) +func init() { + actor.Register(core.RedisCluster, NewJoinNodeActor) +} + func NewJoinNodeActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { return &actorJoinNode{ client: client, @@ -50,10 +56,14 @@ func (a *actorJoinNode) SupportedCommands() []actor.Command { return []actor.Command{cluster.CommandJoinNode} } +func (a *actorJoinNode) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + // Do func (a *actorJoinNode) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { cluster := val.(types.RedisClusterInstance) - logger := a.logger.WithName(cops.CommandJoinNode.String()).WithValues("namespace", cluster.GetNamespace(), "name", cluster.GetName()) + logger := val.Logger().WithValues("actor", cops.CommandJoinNode.String()) // force refresh the cluster if err := cluster.Refresh(ctx); err != nil { @@ -71,7 +81,9 @@ func (a *actorJoinNode) Do(ctx context.Context, val types.RedisInstance) *actor. if node.ContainerStatus() == nil || node.ID() == "" || node.IsTerminating() { continue } - if !node.IsJoined() { + + // if node is not joined or master link is not up, join it + if !node.IsJoined() || !node.IsMasterLinkUp() || node.ClusterInfo().ClusterState != "ok" { unjoined = append(unjoined, node) } else { joined = append(joined, node) @@ -102,9 +114,10 @@ func (a *actorJoinNode) Do(ctx context.Context, val types.RedisInstance) *actor. if err := targetNode.Setup(ctx, margs...); err != nil { return actor.NewResultWithError(cops.CommandAbort, fmt.Errorf("set up cluster meet failed")) } + time.Sleep(time.Second) needRefresh = true } - time.Sleep(time.Second * 5) + time.Sleep(time.Second * 2) } // set update master's replicas @@ -126,7 +139,7 @@ func (a *actorJoinNode) Do(ctx context.Context, val types.RedisInstance) *actor. logger.Info("setup replicate", "pod", node.GetName(), "myid", node.ID(), "role", node.Role(), "masterid", master.ID(), "masterlinkstatus", node.IsMasterLinkUp()) - if node.Role() == redis.RedisRoleMaster && node.Slots().Count(slot.SlotAssigned) > 0 { + if node.Role() == core.RedisRoleMaster && node.Slots().Count(slot.SlotAssigned) > 0 { // if this node is master and has slots, do rebalance return actor.NewResult(cops.CommandRebalance) } @@ -145,48 +158,7 @@ func (a *actorJoinNode) Do(ctx context.Context, val types.RedisInstance) *actor. } else if len(shard.Nodes()) > 0 { // no node is master, no node can be used as new master // all the node is slaves, do force failover to elect a new master - var offset int64 = -1 - for _, node := range shard.Nodes() { - logger.V(2).Info("check node", "node", node.GetName(), - "status", node.IsTerminating(), - "id", node.ID(), - "reploffset", node.Info().MasterReplOffset, - ) - if node.IsTerminating() || node.ID() == "" { - continue - } - if node.Info().MasterReplOffset > offset { - offset = node.Info().MasterReplOffset - master = node - } - } - if master == nil { - // NOTE: below cases will cause this branch: - // 1. pod startup is blocked by scheduler (not enough resource) - // 2. the main container crashed - // when this happends, nothing we can do, just requeue - logger.Info("no node found to setup as master, as if the the container startup blocked or failed") - return actor.NewResult(cops.CommandRequeue) - } - - logger.Info("do replica failover", "importing", shard.IsImporting(), "migrating", shard.IsMigrating()) - if !shard.IsImporting() && !shard.IsMigrating() { - // here use force takeover, for in some cases, - // the node may failed to connect to other nodes, and can't get vote from them. - args := []interface{}{"CLUSTER", "FAILOVER", "FORCE"} - if subRet := func() *actor.ActorResult { - ctx, cancel := context.WithTimeout(ctx, time.Second*10) - defer cancel() - - logger.Info("do replica failover", "node", master.GetName(), "action", args[2]) - if err := master.Setup(ctx, args); err != nil { - logger.Error(err, "do replica failover failed", "node", master.GetName()) - } - return actor.NewResultWithValue(cops.CommandRequeue, time.Second*30) - }(); subRet != nil { - return subRet - } - } + return actor.NewResult(cops.CommandEnsureSlots) } else { // NOTE: below cases will cause this branch: // 1. instance is paused diff --git a/pkg/ops/cluster/actor/actor_rebalance.go b/internal/ops/cluster/actor/actor_rebalance.go similarity index 66% rename from pkg/ops/cluster/actor/actor_rebalance.go rename to internal/ops/cluster/actor/actor_rebalance.go index 3a8c820..15dfb4d 100644 --- a/pkg/ops/cluster/actor/actor_rebalance.go +++ b/internal/ops/cluster/actor/actor_rebalance.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -21,17 +21,24 @@ import ( "fmt" "time" + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + cops "github.com/alauda/redis-operator/internal/ops/cluster" "github.com/alauda/redis-operator/pkg/actor" "github.com/alauda/redis-operator/pkg/kubernetes" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" + rediscli "github.com/alauda/redis-operator/pkg/redis" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" "github.com/go-logr/logr" ) var _ actor.Actor = (*actorRebalance)(nil) +func init() { + actor.Register(core.RedisCluster, NewRebalanceActor) +} + func NewRebalanceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { return &actorRebalance{ client: client, @@ -39,20 +46,16 @@ func NewRebalanceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Ac } } -type SlotMigrateStatus struct { - Slot int - SourceShard int - SourceLabeled bool - DestShard int - DestLabeled bool -} - type actorRebalance struct { client kubernetes.ClientSet logger logr.Logger } +func (a *actorRebalance) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + // SupportedCommands func (a *actorRebalance) SupportedCommands() []actor.Command { return []actor.Command{cops.CommandRebalance} @@ -72,7 +75,19 @@ func (a *actorRebalance) moveSlot(ctx context.Context, destNode, srcNode redis.R return nil } -func (a *actorRebalance) findStableNode(nodes ...redis.RedisNode) redis.RedisNode { +func (a *actorRebalance) stableSlot(ctx context.Context, node redis.RedisNode, slots ...int) *actor.ActorResult { + var args [][]any + for _, slot := range slots { + args = append(args, []any{"CLUSTER", "SETSLOT", slot, "STABLE"}) + } + if err := node.Setup(ctx, args...); err != nil { + a.logger.Error(err, "stable slots failed", "slots", slots) + return actor.NewResultWithError(cops.CommandRequeue, err) + } + return nil +} + +func (a *actorRebalance) findNodeWithMostSlots(nodes ...redis.RedisNode) redis.RedisNode { if len(nodes) == 0 { return nil } @@ -101,6 +116,14 @@ func (a *actorRebalance) findStableNode(nodes ...redis.RedisNode) redis.RedisNod return nodeWithMostSlots } +type SlotMigrateStatus struct { + Slot int + SourceShard int + SourceLabeled bool // true if source shard is labeled in node + DestShard int + DestLabeled bool // true if dest shard is labeled in node +} + // Do // // 关于槽迁移,参考:https://redis.io/commands/cluster-setslot/ @@ -113,42 +136,50 @@ func (a *actorRebalance) findStableNode(nodes ...redis.RedisNode) redis.RedisNod // 3. 即使在槽迁移过程中 node 重启或者关机(可能会数据丢失),operator 会重新标记,sidecar 会重新进行迁移 func (a *actorRebalance) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { cluster := val.(types.RedisClusterInstance) - logger := a.logger.WithName(cops.CommandRebalance.String()).WithValues("namespace", cluster.GetNamespace(), "name", cluster.GetName()) + logger := val.Logger().WithValues("actor", cops.CommandRebalance.String()) if err := cluster.Refresh(ctx); err != nil { + logger.V(3).Error(err, "refresh cluster failed") return actor.NewResultWithError(cops.CommandRequeue, err) } if !cluster.IsReady() { logger.Info("cluster is not ready") - return actor.NewResult(cops.CommandRequeue) + return actor.NewResult(cops.CommandEnsureResource) } - // check is slots fullfilled + // check if slots fullfilled var ( allSlots = slot.NewSlots() shardsSlots = map[int]types.RedisClusterShard{} + nodes = map[string]redis.RedisNode{} ) for _, shard := range cluster.Shards() { allSlots = allSlots.Union(shard.Slots()) shardsSlots[shard.Index()] = shard + + for _, node := range shard.Nodes() { + if node.ID() != "" { + nodes[node.ID()] = node + } + } } // NOTE: 这里的实现基于实例的槽信息的一致性,所以必须强制要求槽无错误 // 同时还要求 cr.status.shards 中记录的槽信息记录的一致性 if !allSlots.IsFullfilled() { - // check if some shard got multi master + // check if some shard got multi master, if so, do slot migrate var nodes []redis.RedisNode for _, shard := range cluster.Shards() { nodes = nodes[0:0] for _, node := range shard.Nodes() { - if node.Role() == redis.RedisRoleMaster && node.Slots().Count(slot.SlotAssigned) > 0 { + if node.Role() == core.RedisRoleMaster && node.Slots().Count(slot.SlotAssigned) > 0 { nodes = append(nodes, node) } } if len(nodes) > 1 { // do slot migrate, check use with node as the final node - destNode := a.findStableNode(nodes...) + destNode := a.findNodeWithMostSlots(nodes...) for _, node := range nodes { if node == destNode { continue @@ -161,20 +192,65 @@ func (a *actorRebalance) Do(ctx context.Context, val types.RedisInstance) *actor } } } + logger.V(3).Info("slots not fullfilled, try to ensure slots") return actor.NewResult(cops.CommandEnsureSlots) } + // check if there is a situation where the migration object is not a master due to failover + nodesWithWrongMigrateFlag := map[redis.RedisNode][]int{} + for _, shard := range cluster.Shards() { + nodeSlots := shard.Slots() + for _, slot := range nodeSlots.SlotsByStatus(slot.SlotMigrating) { + if _, nodeId := nodeSlots.MoveingStatus(slot); nodeId == "" { + continue + } else if node := nodes[nodeId]; node != nil && node.Role() != core.RedisRoleMaster { + nodesWithWrongMigrateFlag[shard.Master()] = append(nodesWithWrongMigrateFlag[shard.Master()], slot) + } + } + for _, slot := range nodeSlots.SlotsByStatus(slot.SlotImporting) { + if _, nodeId := nodeSlots.MoveingStatus(slot); nodeId == "" { + continue + } else if node := nodes[nodeId]; node != nil && node.Role() != core.RedisRoleMaster { + nodesWithWrongMigrateFlag[shard.Master()] = append(nodesWithWrongMigrateFlag[shard.Master()], slot) + } + } + } + if len(nodesWithWrongMigrateFlag) > 0 { + for node, slots := range nodesWithWrongMigrateFlag { + logger.V(3).Info("reset node with wrong slots migrate flag", "node", node.ID(), "slots", slots) + if result := a.stableSlot(ctx, node, slots...); result != nil { + return result + } + } + return actor.NewResult(cops.CommandJoinNode) + } + + // check if there exists master node cluster_state:fail + // if so, stable all importing slots + for _, shard := range cluster.Shards() { + master := shard.Master() + slotsIndex := master.Slots().SlotsByStatus(slot.SlotImporting) + if len(slotsIndex) > 0 && master.ClusterInfo().ClusterState == rediscli.ClusterStateFail { + logger.Error(fmt.Errorf("importing master node %s cluster_state is fail", master.ID()), "master node cluster_state is fail") + if result := a.stableSlot(ctx, master, slotsIndex...); result != nil { + return result + } + return actor.NewResult(cops.CommandRequeue) + } + } + var ( migrateAggMapping = map[int]*SlotMigrateStatus{} migrateAgg = []*SlotMigrateStatus{} ) + // 幂等标记槽的迁移状态 for _, shardStatus := range cluster.Definition().Status.Shards { shard := shardsSlots[int(shardStatus.Index)] currentSlots := shard.Slots() for _, status := range shardStatus.Slots { as := slot.NewSlotAssignStatusFromString(status.Status) - if as == slot.SlotAssigned || as == slot.SlotUnAssigned { + if as == slot.SlotAssigned || as == slot.SlotUnassigned { continue } @@ -209,7 +285,7 @@ func (a *actorRebalance) Do(ctx context.Context, val types.RedisInstance) *actor } } else if as == slot.SlotImporting { for _, slotIndex := range slots.Slots() { - if currentSlots.Status(slotIndex) != slot.SlotUnAssigned && + if currentSlots.Status(slotIndex) != slot.SlotUnassigned && currentSlots.Status(slotIndex) != slot.SlotImporting { continue } diff --git a/pkg/ops/cluster/actor/actor_update_account.go b/internal/ops/cluster/actor/actor_update_account.go similarity index 62% rename from pkg/ops/cluster/actor/actor_update_account.go rename to internal/ops/cluster/actor/actor_update_account.go index 053c951..67b5e6e 100644 --- a/pkg/ops/cluster/actor/actor_update_account.go +++ b/internal/ops/cluster/actor/actor_update_account.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,19 +20,23 @@ import ( "context" "fmt" "reflect" + "slices" "strings" "time" - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + midv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + cops "github.com/alauda/redis-operator/internal/ops/cluster" + "github.com/alauda/redis-operator/internal/util" "github.com/alauda/redis-operator/pkg/actor" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" "github.com/alauda/redis-operator/pkg/security/acl" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/redis" "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -41,6 +45,10 @@ import ( var _ actor.Actor = (*actorUpdateAccount)(nil) +func init() { + actor.Register(core.RedisCluster, NewUpdateAccountActor) +} + func NewUpdateAccountActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { return &actorUpdateAccount{ client: client, @@ -59,6 +67,10 @@ func (a *actorUpdateAccount) SupportedCommands() []actor.Command { return []actor.Command{cops.CommandUpdateAccount} } +func (a *actorUpdateAccount) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + // Do // // 对于账户更新,需要尽量保持在一个 reconcile 里完成,否则会出现一个实例多种密码的情况 @@ -73,8 +85,10 @@ func (a *actorUpdateAccount) SupportedCommands() []actor.Command { // 2. 更新密码不能更新 secret,需要新建 secret func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { cluster := val.(types.RedisClusterInstance) - logger := a.logger.WithName(cops.CommandUpdateAccount.String()).WithValues("namespace", cluster.GetNamespace(), "name", cluster.GetName()) + logger := val.Logger().WithValues("actor", cops.CommandUpdateAccount.String()) + logger.Info("start update account", "cluster", cluster.GetName()) + var ( err error @@ -84,7 +98,7 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a ) if defaultUser == nil { - defaultUser, _ = user.NewUser("", user.RoleDeveloper, nil) + defaultUser, _ = user.NewUser("", user.RoleDeveloper, nil, cluster.Version().IsACL2Supported()) } var ( @@ -111,7 +125,7 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a Labels: cluster.GetLabels(), OwnerReferences: util.BuildOwnerReferences(cluster.Definition()), }, - Data: users.Encode(), + Data: users.Encode(true), } // create acl with old password @@ -132,23 +146,7 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a } } - var newSecret *corev1.Secret - if newSecretName != "" { - if newSecret, err = a.client.GetSecret(ctx, cluster.GetNamespace(), newSecretName); errors.IsNotFound(err) { - logger.Error(err, "get cluster secret failed", "target", newSecretName) - return actor.NewResultWithError(cops.CommandRequeue, fmt.Errorf("secret %s not found", newSecretName)) - - } else if err != nil { - logger.Error(err, "get cluster secret failed", "target", newSecretName) - return actor.NewResultWithError(cops.CommandRequeue, err) - } - } - isUpdated := false - if newSecretName != currentSecretName { - defaultUser.Password, _ = user.NewPassword(newSecret) - isUpdated = true - } users = append(users[0:0], defaultUser) if cluster.Version().IsACLSupported() { if !isAclEnabled { @@ -163,74 +161,78 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a users = append(users, opUser) isUpdated = true } - opRedisUser := clusterbuilder.GenerateClusterOperatorsRedisUser(cluster, secretName) - if err := a.client.CreateIfNotExistsRedisUser(ctx, &opRedisUser); err != nil { + opRedisUser := clusterbuilder.GenerateClusterRedisUser(cluster, opUser) + if err := a.client.CreateIfNotExistsRedisUser(ctx, opRedisUser); err != nil { logger.Error(err, "create operator redis user failed") return actor.NewResult(cops.CommandRequeue) } - } - - // append renames to default user - renameVal := cluster.Definition().Spec.Config[clusterbuilder.RedisConfig_RenameCommand] - renames, _ := clusterbuilder.ParseRenameConfigs(renameVal) - rule := user.Rule{ - Categories: []string{"all"}, - KeyPatterns: []string{"*"}, - } - for key, val := range renames { - if key == val { - continue - } - if !func() bool { - for _, k := range rule.DisallowedCommands { - if k == key { - return true - } + cluster.SendEventf(corev1.EventTypeNormal, config.EventCreateUser, "created operator user to enable acl") + } else { + if newOpUser, err := acl.NewOperatorUser(ctx, a.client, + opUser.Password.SecretName, cluster.GetNamespace(), nil, cluster.Version().IsACL2Supported()); err != nil { + logger.Error(err, "create operator user failed") + return actor.NewResult(cops.CommandRequeue) + } else { + opRedisUser := clusterbuilder.GenerateClusterRedisUser(cluster, newOpUser) + if err := a.client.CreateOrUpdateRedisUser(ctx, opRedisUser); err != nil { + logger.Error(err, "update operator redis user failed") + return actor.NewResult(cops.CommandRequeue) } - return false - }() { - rule.DisallowedCommands = append(rule.DisallowedCommands, key) + cluster.SendEventf(corev1.EventTypeNormal, config.EventCreateUser, "created/updated operator user") + + opUser.Rules = newOpUser.Rules + users = append(users, opUser) + + isUpdated = true } } - defaultUser.Rules = append(defaultUser.Rules[0:0], &rule) - if !cluster.IsACLUserExists() { - defaultRedisUser := clusterbuilder.GenerateClusterDefaultRedisUser(cluster.Definition(), "") - if defaultUser.Password != nil { - defaultRedisUser = clusterbuilder.GenerateClusterDefaultRedisUser(cluster.Definition(), defaultUser.Password.GetSecretName()) - } - if err := a.client.CreateIfNotExistsRedisUser(ctx, &defaultRedisUser); err != nil { - logger.Error(err, "create default redis user failed") + defaultRedisUser := clusterbuilder.GenerateClusterRedisUser(cluster, defaultUser) + defaultRedisUser.Annotations[midv1.ACLSupportedVersionAnnotationKey] = cluster.Version().String() + if oldDefaultRU, err := a.client.GetRedisUser(ctx, cluster.GetNamespace(), defaultRedisUser.GetName()); errors.IsNotFound(err) { + if err := a.client.CreateIfNotExistsRedisUser(ctx, defaultRedisUser); err != nil { + logger.Error(err, "update default redis user failed") return actor.NewResult(cops.CommandRequeue) } - opRedisUser := clusterbuilder.GenerateClusterOperatorsRedisUser(cluster, "") - if opUser.Password != nil { - opRedisUser = clusterbuilder.GenerateClusterOperatorsRedisUser(cluster, opUser.Password.GetSecretName()) - } - if err := a.client.CreateIfNotExistsRedisUser(ctx, &opRedisUser); err != nil { - logger.Error(err, "create operator redis user failed") - return actor.NewResult(cops.CommandRequeue) + cluster.SendEventf(corev1.EventTypeNormal, config.EventCreateUser, "created default user") + } else if err != nil { + logger.Error(err, "get default redisuser failed") + return actor.NewResult(cops.CommandRequeue) + } else if cluster.Version().IsACL2Supported() { + oldVersion := redis.RedisVersion(oldDefaultRU.Annotations[midv1.ACLSupportedVersionAnnotationKey]) + // COMP: if old version not support acl2, and new version is supported, update acl rules for compatibility + if !oldVersion.IsACL2Supported() { + fields := strings.Fields(oldDefaultRU.Spec.AclRules) + if !slices.Contains(fields, "&*") && !slices.Contains(fields, "allchannels") { + oldDefaultRU.Spec.AclRules = fmt.Sprintf("%s &*", oldDefaultRU.Spec.AclRules) + } + if oldDefaultRU.Annotations == nil { + oldDefaultRU.Annotations = make(map[string]string) + } + oldDefaultRU.Annotations[midv1.ACLSupportedVersionAnnotationKey] = cluster.Version().String() + if err := a.client.UpdateRedisUser(ctx, oldDefaultRU); err != nil { + logger.Error(err, "update default redis user failed") + return actor.NewResult(cops.CommandRequeue) + } + cluster.SendEventf(corev1.EventTypeNormal, config.EventUpdateUser, "migrate default user acl rules to support channels") } - } - } - for k, v := range users.Encode() { - oldCm.Data[k] = v + if !reflect.DeepEqual(users.Encode(true), oldCm.Data) { + isUpdated = true } if isUpdated { - // update configmap - if cluster.Version().IsACLSupported() { - if err := a.client.CreateIfNotExistsConfigMap(ctx, cluster.GetNamespace(), oldCm); err != nil { - logger.Error(err, "update acl configmap failed", "target", oldCm.Name) - return actor.NewResultWithError(cops.CommandRequeue, err) - } - } else { - if err := a.client.CreateOrUpdateConfigMap(ctx, cluster.GetNamespace(), oldCm); err != nil { - logger.Error(err, "update acl configmap failed", "target", oldCm.Name) - return actor.NewResultWithError(cops.CommandRequeue, err) - } + for k, v := range users.Encode(true) { + oldCm.Data[k] = v + } + if err := a.client.CreateOrUpdateConfigMap(ctx, cluster.GetNamespace(), oldCm); err != nil { + logger.Error(err, "update acl configmap failed", "target", oldCm.Name) + return actor.NewResultWithError(cops.CommandRequeue, err) + } + if err := cluster.Refresh(ctx); err != nil { + logger.Error(err, "refresh resource failed") + return actor.NewResultWithError(cops.CommandRequeue, err) } } @@ -288,7 +290,7 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a } // update default user password - ru := clusterbuilder.GenerateClusterDefaultRedisUser(cluster.Definition(), newSecretName) + ru := clusterbuilder.GenerateClusterRedisUser(cluster, defaultUser) oldRu, err := a.client.GetRedisUser(ctx, cluster.GetNamespace(), ru.Name) if err == nil && !reflect.DeepEqual(oldRu.Spec.PasswordSecrets, ru.Spec.PasswordSecrets) { oldRu.Spec.PasswordSecrets = ru.Spec.PasswordSecrets @@ -296,11 +298,11 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a logger.Error(err, "update default user redisUser failed") } } else if errors.IsNotFound(err) { - if err := a.client.CreateIfNotExistsRedisUser(ctx, &ru); err != nil { + if err := a.client.CreateIfNotExistsRedisUser(ctx, ru); err != nil { logger.Error(err, "create default user redisUser failed") } + cluster.SendEventf(corev1.EventTypeNormal, config.EventCreateUser, "created default user when update password") } - } else { // this is low probability event @@ -325,6 +327,7 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a logger.Error(err, "update acl config failed") } } + cluster.SendEventf(corev1.EventTypeNormal, config.EventUpdatePassword, "updated instance password and injected acl users") // then requeue to refresh cluster info a.logger.Info("=== requeue to refresh cluster info ===(acl)") return actor.NewResultWithValue(cops.CommandRequeue, time.Second) @@ -341,42 +344,46 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a []interface{}{"config", "set", "masteruser", opUser.Name}, []interface{}{"config", "set", "masterauth", opUser.Password.String()}, ) + allAclUpdated := true for _, node := range cluster.Nodes() { if node.ContainerStatus() == nil || !node.ContainerStatus().Ready || node.IsTerminating() { + allAclUpdated = false continue } if err := node.Setup(ctx, margs...); err != nil { + allAclUpdated = false logger.Error(err, "update acl config failed") } } - - if err := a.client.CreateOrUpdateConfigMap(ctx, cluster.GetNamespace(), oldCm); err != nil { - logger.Error(err, "update acl configmap failed", "target", oldCm.Name) - return actor.NewResultWithError(cops.CommandRequeue, err) + if allAclUpdated { + if err := a.client.CreateOrUpdateConfigMap(ctx, cluster.GetNamespace(), oldCm); err != nil { + logger.Error(err, "update acl configmap failed", "target", oldCm.Name) + return actor.NewResultWithError(cops.CommandRequeue, err) + } + cluster.SendEventf(corev1.EventTypeNormal, config.EventUpdatePassword, "applied acl to all pods, switch to operator user for cluster auth") } - a.logger.Info("=== requeue to refresh cluster info ===(no acl)") // then requeue to refresh cluster info return actor.NewResultWithValue(cops.CommandRequeue, time.Second) } } else { if newSecretName != currentSecretName { - // NOTE: failover first before update password - logger.Info("failover before update password to force slave to reconnect to master after restart") - for _, shard := range cluster.Shards() { - for _, node := range shard.Nodes() { - if !node.IsReady() || node.IsTerminating() || node.Role() == redis.RedisRoleMaster { - break - } else { - _ = node.Setup(ctx, []interface{}{"cluster", "failover"}) - time.Sleep(time.Second * 5) - break - } + var newSecret *corev1.Secret + if newSecretName != "" { + if newSecret, err = a.client.GetSecret(ctx, cluster.GetNamespace(), newSecretName); errors.IsNotFound(err) { + logger.Error(err, "get cluster secret failed", "target", newSecretName) + return actor.NewResultWithError(cops.CommandRequeue, fmt.Errorf("secret %s not found", newSecretName)) + + } else if err != nil { + logger.Error(err, "get cluster secret failed", "target", newSecretName) + return actor.NewResultWithError(cops.CommandRequeue, err) } } + defaultUser.Password, _ = user.NewPassword(newSecret) + _ = cluster.UpdateStatus(ctx, types.Any, "updating password") // update masterauth and requirepass, and restart (ensure_resource do this) // hotconfig with redis acl/password updateMasterAuth := []interface{}{"config", "set", "masterauth", defaultUser.Password.String()} @@ -403,16 +410,38 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a } } - // NOTE: - // NOTE: - // NOTE: after updated masterauth, the slave will not use this new value to connect to master - // we must wait about 60s (repl-timeout) for them to take effect - // logger.Info(fmt.Sprintf("wait %ds repl-timeout for slave to reconnect to master", timeout)) + for k, v := range users.Encode(true) { + oldCm.Data[k] = v + } + if err := a.client.CreateOrUpdateConfigMap(ctx, cluster.GetNamespace(), oldCm); err != nil { + logger.Error(err, "update acl configmap failed", "target", oldCm.Name) + return actor.NewResultWithError(cops.CommandRequeue, err) + } + + if err := cluster.Refresh(ctx); err != nil { + logger.Error(err, "refresh resource failed") + } + + // kill all replica clients from master to force replicas use new password reconnect to master + for _, shard := range cluster.Shards() { + master := shard.Master() + if master == nil { + continue + } + + logger.Info("force replica clients to reconnect to master", "master", master.GetName()) + // NOTE: require redis 5.0 + if err := master.Setup(ctx, []interface{}{"client", "kill", "type", "replica"}); err != nil { + logger.Error(err, "kill replica client failed", "master", master.GetName()) + } + time.Sleep(time.Second * 5) + } + cluster.SendEventf(corev1.EventTypeNormal, config.EventUpdatePassword, "updated instance password") - _ = cluster.UpdateStatus(ctx, v1alpha1.ClusterStatusRollingUpdate, "updating password", nil) - return nil + return actor.NewResult(cops.CommandRequeue) } } + if cluster.Version().IsACLSupported() && !isAllACLSupported { return actor.NewResult(cops.CommandEnsureResource) } @@ -422,69 +451,24 @@ func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *a // formatACLSetCommand // // only acl 1 supported -func formatACLSetCommand(u *user.User) (args []interface{}) { +func formatACLSetCommand(u *user.User) []interface{} { if u == nil { return nil } - if len(u.Rules) == 0 { - _ = u.AppendRule(&user.Rule{ - Categories: []string{"all"}, - KeyPatterns: []string{"*"}, - }) - } + // keep in mind that the user.Name is "default" for default user // when update command,password,keypattern, must reset them all - args = append(args, "acl", "setuser", u.Name, "reset") + args := []interface{}{"acl", "setuser", u.Name, "reset"} for _, rule := range u.Rules { - for _, cate := range rule.Categories { - cate = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(cate), "+"), "@") - args = append(args, fmt.Sprintf("+@%s", cate)) - } - for _, cmd := range rule.AllowedCommands { - cmd = strings.TrimPrefix(cmd, "+") - args = append(args, fmt.Sprintf("+%s", cmd)) - } - - isDisableAllCmd := false - for _, cmd := range rule.DisallowedCommands { - cmd = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(cmd), "-"), "@") - if cmd == "nocommands" || cmd == "-@all" { - isDisableAllCmd = true - } - args = append(args, fmt.Sprintf("-%s", cmd)) - } - if len(rule.Categories) == 0 && len(rule.AllowedCommands) == 0 && !isDisableAllCmd { - args = append(args, "+@all") + for _, field := range strings.Fields(rule.Encode()) { + args = append(args, field) } - - if len(rule.KeyPatterns) == 0 { - rule.KeyPatterns = append(rule.KeyPatterns, "*") - } - for _, pattern := range rule.KeyPatterns { - pattern = strings.TrimPrefix(strings.TrimSpace(pattern), "~") - // Reference: https://raw.githubusercontent.com/antirez/redis/7.0/redis.conf - if !strings.HasPrefix(pattern, "%") { - pattern = fmt.Sprintf("~%s", pattern) - } - args = append(args, pattern) - } - for _, pattern := range rule.Channels { - pattern = strings.TrimPrefix(strings.TrimSpace(pattern), "&") - // Reference: https://raw.githubusercontent.com/antirez/redis/7.0/redis.conf - if !strings.HasPrefix(pattern, "&") { - pattern = fmt.Sprintf("&%s", pattern) - } - args = append(args, pattern) - } - passwd := u.Password.String() - if passwd == "" { - args = append(args, "nopass") - } else { - args = append(args, fmt.Sprintf(">%s", passwd)) - } - - // NOTE: on must after reset - args = append(args, "on") } - return + passwd := u.Password.String() + if passwd == "" { + args = append(args, "nopass") + } else { + args = append(args, fmt.Sprintf(">%s", passwd)) + } + return append(args, "on") } diff --git a/pkg/ops/cluster/actor/actor_update_config.go b/internal/ops/cluster/actor/actor_update_config.go similarity index 88% rename from pkg/ops/cluster/actor/actor_update_config.go rename to internal/ops/cluster/actor/actor_update_config.go index ddab300..b86a37e 100644 --- a/pkg/ops/cluster/actor/actor_update_config.go +++ b/internal/ops/cluster/actor/actor_update_config.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,10 +19,12 @@ package actor import ( "context" + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + cops "github.com/alauda/redis-operator/internal/ops/cluster" "github.com/alauda/redis-operator/pkg/actor" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" "github.com/alauda/redis-operator/pkg/types" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/errors" @@ -31,6 +33,10 @@ import ( var _ actor.Actor = (*actorUpdateConfig)(nil) +func init() { + actor.Register(core.RedisCluster, NewUpdateConfigActor) +} + func NewUpdateConfigActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { return &actorUpdateConfig{ client: client, @@ -49,12 +55,17 @@ func (a *actorUpdateConfig) SupportedCommands() []actor.Command { return []actor.Command{cops.CommandUpdateConfig} } +func (a *actorUpdateConfig) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + // Do // // two type config: hotconfig and restartconfig // use cm to check the difference of the config func (a *actorUpdateConfig) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - logger := a.logger.WithName(cops.CommandEnsureResource.String()).WithValues("namespace", val.GetNamespace(), "name", val.GetName()) + logger := val.Logger().WithValues("actor", cops.CommandUpdateConfig.String()) + cluster := val.(types.RedisClusterInstance) newCm, _ := clusterbuilder.NewConfigMapForCR(cluster) oldCm, err := a.client.GetConfigMap(ctx, newCm.Namespace, newCm.Name) diff --git a/internal/ops/cluster/command.go b/internal/ops/cluster/command.go new file mode 100644 index 0000000..b8cc2c1 --- /dev/null +++ b/internal/ops/cluster/command.go @@ -0,0 +1,37 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cluster + +import ( + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/pkg/actor" +) + +var ( + CommandRequeue = actor.CommandRequeue + CommandAbort = actor.CommandAbort + CommandPaused = actor.CommandPaused + + CommandUpdateAccount actor.Command = actor.NewCommand(core.RedisCluster, "CommandUpdateAccount") + CommandUpdateConfig = actor.NewCommand(core.RedisCluster, "CommandUpdateConfig") + CommandEnsureResource = actor.NewCommand(core.RedisCluster, "CommandEnsureResource") + CommandHealPod = actor.NewCommand(core.RedisCluster, "CommandHealPod") + CommandCleanResource = actor.NewCommand(core.RedisCluster, "CommandCleanResource") + CommandJoinNode = actor.NewCommand(core.RedisCluster, "CommandJoinNode") + CommandEnsureSlots = actor.NewCommand(core.RedisCluster, "CommandEnsureSlots") + CommandRebalance = actor.NewCommand(core.RedisCluster, "CommandRebalance") +) diff --git a/pkg/ops/cluster/engine.go b/internal/ops/cluster/engine.go similarity index 72% rename from pkg/ops/cluster/engine.go rename to internal/ops/cluster/engine.go index 49d27bc..e158e14 100644 --- a/pkg/ops/cluster/engine.go +++ b/internal/ops/cluster/engine.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,18 +20,25 @@ import ( "context" "fmt" "reflect" + "slices" + "strings" "time" - clusterv1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + midv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/config" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" + "github.com/alauda/redis-operator/pkg/security/acl" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -66,18 +73,16 @@ func NewRuleEngine(client kubernetes.ClientSet, eventRecorder record.EventRecord // Inspect func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - if g == nil { - return nil - } + logger := val.Logger() + cluster := val.(types.RedisClusterInstance) - logger := g.logger.WithName("Inspect").WithValues("namespace", cluster.GetNamespace(), "name", cluster.GetName()) if cluster == nil { logger.Info("cluster is nil") return nil } cr := cluster.Definition() - if (cr.Spec.Annotations != nil) && cr.Spec.Annotations[config.PAUSE_ANNOTATION_KEY] != "" { + if cr.Spec.PodAnnotations[config.PAUSE_ANNOTATION_KEY] != "" { return actor.NewResult(CommandEnsureResource) } @@ -118,7 +123,11 @@ func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *acto check_2 := func() *actor.ActorResult { logger.V(3).Info("check_2", "shards", len(cluster.Shards()), "desired", cr.Spec.MasterSize) - if int(cr.Spec.MasterSize) > len(cluster.Shards()) { + isFullfilled, err := cluster.IsResourceFullfilled(ctx) + if err != nil { + return actor.RequeueWithError(err) + } + if int(cr.Spec.MasterSize) > len(cluster.Shards()) || !isFullfilled { // 2.1 if missing shard return actor.NewResult(CommandEnsureResource) } @@ -160,9 +169,9 @@ func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *acto } // check if exists nodes not joined as replica of master - if node.Role() == redis.RedisRoleMaster { + if node.Role() == core.RedisRoleMaster { masterCount += 1 - } else if node.Role() == redis.RedisRoleSlave { + } else if node.Role() == core.RedisRoleReplica { if master != nil { if node.MasterID() != master.ID() { needNodeReplicas = true @@ -186,7 +195,11 @@ func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *acto } } - if needNodeReplicas = (len(shard.Nodes()) > 0 && masterCount != 1) || needNodeReplicas; needNodeReplicas { + // multi master may cause by below condition + // a. the master node is not joined + // b. the shard is in split brain state + // TODO split brain fix may cause data loss, we should do more check + if needNodeReplicas = (masterCount > 1) || needNodeReplicas; needNodeReplicas { break } } @@ -215,7 +228,7 @@ func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *acto } for _, node := range shard.Nodes() { - if node.Role() == redis.RedisRoleMaster && !node.IsJoined() { + if node.Role() == core.RedisRoleMaster && !node.IsJoined() { // 4.2 check if every shards has only one master return actor.NewResult(CommandJoinNode) } @@ -277,17 +290,52 @@ func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *acto node := shard.Nodes()[i] // 5.2 long time termination pod found - if node.IsTerminating() && now.Sub(node.GetDeletionTimestamp().Time) >= time.Second*30 { + if node.IsTerminating() && + now.After(node.GetDeletionTimestamp(). + Add(time.Duration(*node.GetDeletionGracePeriodSeconds())*time.Second)) { return actor.NewResult(CommandHealPod) } + // 5.3 long time pending pod found - if node.Status() == corev1.PodPending && - now.Sub(node.GetCreationTimestamp().Time) > time.Second*30 { - return actor.NewResult(CommandHealPod) - } + // if node.Status() == corev1.PodPending && + // now.Sub(node.GetCreationTimestamp().Time) > time.Second*30 { + // return actor.NewResult(CommandHealPod) + // } // 5.4 pod failed // TODO: pod fail is hard to check, not clear what caused the fail + + // 5.5 pod announce ip/port consistent with svc + if typ := cr.Spec.Expose.ServiceType; node.IsReady() && + (typ == corev1.ServiceTypeNodePort || typ == corev1.ServiceTypeLoadBalancer) { + svc, err := g.client.GetService(ctx, cr.Namespace, node.GetName()) + if errors.IsNotFound(err) { + logger.Info("service not found", "service", node.GetName()) + return actor.NewResult(CommandEnsureResource) + } else if err != nil { + logger.Error(err, "get service failed", "service", node.GetName()) + return actor.RequeueWithError(err) + } + announceIP := node.DefaultIP().String() + announcePort := node.Port() + + if typ == corev1.ServiceTypeNodePort { + port := util.GetServicePortByName(svc, "client") + if port != nil { + if int(port.NodePort) != announcePort { + return actor.NewResult(CommandHealPod) + } + } else { + logger.Error(fmt.Errorf("service %s not found", node.GetName()), "failed to get service, which should not happen") + } + } else if typ == corev1.ServiceTypeLoadBalancer { + if slices.IndexFunc(svc.Status.LoadBalancer.Ingress, func(ing corev1.LoadBalancerIngress) bool { + return ing.IP == announceIP + }) < 0 { + return actor.NewResult(CommandHealPod) + } + } + } } } return nil @@ -295,7 +343,7 @@ func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *acto if ret := check_5(); ret != nil { return ret } - return nil + return actor.NewResult(CommandEnsureResource) } // allocateSlots @@ -303,10 +351,14 @@ func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *acto // a. new create shards without slots assigned // b. the slots is all assigned and the cluster is not scaling up/down func (g *RuleEngine) allocateSlots(ctx context.Context, cluster types.RedisClusterInstance) *actor.ActorResult { + logger := g.logger cr := cluster.Definition() if len(cr.Status.Shards) == 0 { - var newSlots []*slot.Slots + var ( + newSlots []*slot.Slots + msg string = "loaded(load from upgrade)" + ) for _, shard := range cluster.Shards() { if s := shard.Slots(); s != nil { newSlots = append(newSlots, s) @@ -322,26 +374,35 @@ func (g *RuleEngine) allocateSlots(ctx context.Context, cluster types.RedisClust newSlots = append(newSlots, shardSlots) } } + msg = "manual configed" } else { newSlots = slot.Allocate(int(cr.Spec.MasterSize), nil) + msg = "default allocated" } } else if !slot.IsFullfilled(newSlots...) { // NOTE: refuse to assign slots because the old instance is not healthy // the new operator relies to shards assign. if a unhealthy instance comes, we cann't reconcile it. err := fmt.Errorf("cann't take over unhealthy instance, please fix the instance first") - if err := cluster.UpdateStatus(ctx, clusterv1.ClusterStatusKO, err.Error(), nil); err != nil { - return actor.NewResultWithError(CommandRequeue, err) + if err := cluster.UpdateStatus(ctx, types.Fail, err.Error()); err != nil { + return actor.RequeueWithError(err) } return actor.NewResultWithError(CommandAbort, err) } - shardStatus := buildStatusOfShards(cluster, newSlots) - g.eventRecorder.Eventf(cluster.Definition(), corev1.EventTypeNormal, - "PreassignSlots", "preassigned slots for %d shards", cr.Spec.MasterSize) + shardStatus := buildStatusOfShards(cluster, newSlots) + if err := cluster.RewriteShards(ctx, shardStatus); err != nil { + return actor.RequeueWithError(err) + } - if err := cluster.UpdateStatus(ctx, clusterv1.ClusterStatusCreating, "", shardStatus); err != nil { - return actor.NewResultWithError(CommandRequeue, err) + { + var slotsStr []string + for _, s := range newSlots { + slotsStr = append(slotsStr, s.String()) + } + cluster.SendEventf(corev1.EventTypeNormal, config.EventAllocateSlots, + "assign %s slots for %d shards: %s", + msg, cr.Spec.MasterSize, strings.Join(slotsStr, ";")) } } @@ -350,24 +411,29 @@ func (g *RuleEngine) allocateSlots(ctx context.Context, cluster types.RedisClust return nil } - for _, shard := range cluster.Definition().Status.Shards { - for _, status := range shard.Slots { - if status.Status == slot.SlotAssigned.String() { - continue + needUpdateShardStatus := func() bool { + for _, shard := range cluster.Definition().Status.Shards { + for _, status := range shard.Slots { + if status.Status != slot.SlotAssigned.String() { + return true + } } - shardStatus := buildStatusOfShards(cluster, nil) - status := cr.Status - status.Shards = shardStatus - status.Status = clusterv1.ClusterStatusRebalancing - status.ClusterStatus = clusterv1.ClusterInService + } + return false + }() + + if needUpdateShardStatus { + shardStatus := buildStatusOfShards(cluster, nil) - g.eventRecorder.Eventf(cluster.Definition(), corev1.EventTypeNormal, - "UpdateSlotsStatus", "update shards slots status") - if err := cluster.UpdateStatus(ctx, clusterv1.ClusterStatusRebalancing, "", shardStatus); err != nil { - return actor.NewResultWithError(CommandRequeue, err) + if !reflect.DeepEqual(cluster.Definition().Status.Shards, shardStatus) { + logger.V(3).Info("rebalance slots", "shards", cluster.Definition().Status.Shards, "new shards", shardStatus) + if err := cluster.RewriteShards(ctx, shardStatus); err != nil { + return actor.RequeueWithError(err) } - return nil + return actor.Requeue() } + // NOTE: if cluster is already in rebalance state, we should wait for the rebalance finish before next scaling + return nil } if currentSize := len(cluster.Definition().Status.Shards); currentSize != int(cr.Spec.MasterSize) { @@ -378,18 +444,29 @@ func (g *RuleEngine) allocateSlots(ctx context.Context, cluster types.RedisClust } } - g.eventRecorder.Eventf(cluster.Definition(), corev1.EventTypeNormal, - "RebalanceSlots", "rebalance slots assigned status because of scaling %d=>%d", - currentSize, cr.Spec.MasterSize) - - for i, oldSlot := range oldSlots { - fmt.Printf("old=index: %d, sots: %s\n", i, oldSlot) - } // scale up/down newSlots := slot.Allocate(int(cr.Spec.MasterSize), oldSlots) shardStatus := buildStatusOfShards(cluster, newSlots) - if err := cluster.UpdateStatus(ctx, clusterv1.ClusterStatusRebalancing, "", shardStatus); err != nil { - return actor.NewResultWithError(CommandRequeue, err) + if err := cluster.RewriteShards(ctx, shardStatus); err != nil { + return actor.RequeueWithError(err) + } + + { + var ( + oldSlotsStr []string + newSlotsStr []string + ) + for _, s := range oldSlots { + oldSlotsStr = append(oldSlotsStr, s.String()) + } + for _, s := range newSlots { + newSlotsStr = append(newSlotsStr, s.String()) + } + logger.Info("rebalance slots", "oldSlots", oldSlots, "newSlots", newSlots) + cluster.SendEventf(corev1.EventTypeNormal, config.EventRebalance, + "scaling instanace %d=>%d, rebalance slots %s => %s", + currentSize, cr.Spec.MasterSize, + strings.Join(oldSlotsStr, ";"), strings.Join(newSlotsStr, ";")) } } @@ -416,8 +493,10 @@ func (g *RuleEngine) isPasswordChanged(ctx context.Context, cluster types.RedisC if cr.Spec.PasswordSecret != nil && cr.Spec.PasswordSecret.Name != "" { currentSecretName = cr.Spec.PasswordSecret.Name } + opUser := users.GetOpUser() defaultUser := users.GetDefaultUser() - if defaultUser.GetPassword().GetSecretName() != currentSecretName { + // UPGRADE: use RedisUser controller to manage the user update + if defaultUser.GetPassword().GetSecretName() != currentSecretName && !cluster.Version().IsACLSupported() { return true, nil } @@ -437,6 +516,47 @@ func (g *RuleEngine) isPasswordChanged(ctx context.Context, cluster types.RedisC if cluster.Version().IsACLSupported() && !cluster.IsACLUserExists() { return true, nil } + if cluster.Version().IsACL2Supported() { + if opUser.Role == user.RoleOperator && (len(opUser.Rules) == 0 || len(opUser.Rules[0].Channels) == 0) { + return true, nil + } + } + + cmName := clusterbuilder.GenerateClusterACLConfigMapName(cluster.GetName()) + if cm, err := g.client.GetConfigMap(ctx, cluster.GetNamespace(), cmName); errors.IsNotFound(err) { + return true, nil + } else if err != nil { + g.logger.Error(err, "failed to get configmap", "configmap", cmName) + return false, err + } else if users, err := acl.LoadACLUsers(ctx, g.client, cm); err != nil { + logger.Error(err, "load acl users failed", "target", client.ObjectKey{Namespace: cluster.GetNamespace(), Name: cmName}) + return false, err + } else { + if cluster.Version().IsACL2Supported() { + opUser := users.GetOpUser() + g.logger.V(3).Info("check acl2 support", "role", opUser.Role, "rules", opUser.Rules) + if opUser.Role == user.RoleOperator && (len(opUser.Rules) == 0 || len(opUser.Rules[0].Channels) == 0) { + g.logger.Info("operator user has no channel rules") + return true, nil + } + + defaultRUName := clusterbuilder.GenerateClusterRedisUserName(cluster.GetName(), defaultUser.Name) + if defaultRU, err := g.client.GetRedisUser(ctx, cluster.GetNamespace(), defaultRUName); errors.IsNotFound(err) { + return true, nil + } else if err != nil { + return false, err + } else { + oldVersion := redis.RedisVersion(defaultRU.Annotations[midv1.ACLSupportedVersionAnnotationKey]) + if !oldVersion.IsACL2Supported() { + return true, nil + } + } + } + if !reflect.DeepEqual(users.Encode(true), users.Encode(false)) { + g.logger.V(3).Info("not equal", "new", users.Encode(true), "old", users.Encode(false)) + return true, nil + } + } return false, nil } @@ -444,7 +564,7 @@ func (g *RuleEngine) isCustomServerChanged(ctx context.Context, cluster types.Re portsMap := make(map[int32]bool) cr := cluster.Definition() - if cr.Spec.Expose.DataStorageNodePortSequence == "" { + if cr.Spec.Expose.NodePortSequence == "" { return false, nil } labels := clusterbuilder.GetClusterLabels(cr.Name, nil) @@ -452,7 +572,7 @@ func (g *RuleEngine) isCustomServerChanged(ctx context.Context, cluster types.Re if err != nil { return true, err } - ports, err := util.ParsePortSequence(cr.Spec.Expose.DataStorageNodePortSequence) + ports, err := helper.ParseSequencePorts(cr.Spec.Expose.NodePortSequence) for _, v := range ports { portsMap[v] = true } @@ -461,7 +581,7 @@ func (g *RuleEngine) isCustomServerChanged(ctx context.Context, cluster types.Re } nodePortMap := make(map[int32]bool) for _, svc := range svcs.Items { - if svc.Spec.Selector["statefulset.kubernetes.io/pod-name"] != "" { + if svc.Spec.Selector[builder.PodNameLabelKey] != "" { if len(svc.Spec.Ports) > 1 { nodePortMap[svc.Spec.Ports[0].NodePort] = true } @@ -496,9 +616,6 @@ func (g *RuleEngine) isConfigMapChanged(ctx context.Context, cluster types.Redis } func buildStatusOfShards(cluster types.RedisClusterInstance, slots []*slot.Slots) (ret []*clusterv1.ClusterShards) { - for i, slot := range slots { - fmt.Printf("new: index: %d, slot: %s\n", i, slot) - } statusShards := cluster.Definition().Status.Shards maxShards := len(cluster.Shards()) if int(cluster.Definition().Spec.MasterSize) > maxShards { @@ -623,7 +740,7 @@ func buildStatusOfShards(cluster types.RedisClusterInstance, slots []*slot.Slots _ = tmpSlots.Set(status.Slots, slot.SlotAssigned) for _, slotIndex := range tmpSlots.Slots() { if shardIndex, ok := slotsCurrentShard[slotIndex]; ok && shardIndex == int(*status.ShardIndex) { - _ = assignedSlots.Set(slotIndex, slot.SlotUnAssigned) + _ = assignedSlots.Set(slotIndex, slot.SlotUnassigned) } else { if tmpSlot := migratingSubGroup[int(*status.ShardIndex)]; tmpSlot == nil { migratingSubGroup[int(*status.ShardIndex)] = slot.NewSlots() diff --git a/pkg/ops/cluster/engine_test.go b/internal/ops/cluster/engine_test.go similarity index 97% rename from pkg/ops/cluster/engine_test.go rename to internal/ops/cluster/engine_test.go index bd5caea..4943136 100644 --- a/pkg/ops/cluster/engine_test.go +++ b/internal/ops/cluster/engine_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -22,10 +22,10 @@ import ( "sort" "testing" - clusterv1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/models/cluster" + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/internal/redis/cluster" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/slot" "k8s.io/utils/pointer" ) diff --git a/internal/ops/failover/actor/actor_clean_resource.go b/internal/ops/failover/actor/actor_clean_resource.go new file mode 100644 index 0000000..405abeb --- /dev/null +++ b/internal/ops/failover/actor/actor_clean_resource.go @@ -0,0 +1,107 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + + "github.com/alauda/redis-operator/api/core" + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + ops "github.com/alauda/redis-operator/internal/ops/failover" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/Masterminds/semver/v3" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" +) + +var _ actor.Actor = (*actorCleanResource)(nil) + +func init() { + actor.Register(core.RedisSentinel, NewCleanResourceActor) +} + +func NewCleanResourceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorCleanResource{ + client: client, + logger: logger, + } +} + +type actorCleanResource struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorCleanResource) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandCleanResource} +} + +func (a *actorCleanResource) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +// Do +func (a *actorCleanResource) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandCleanResource.String()) + + inst := val.(types.RedisFailoverInstance) + cr := inst.Definition() + + if inst.IsReady() { + // TODO: deprecated in 3.22 + name := sentinelbuilder.GetSentinelStatefulSetName(inst.GetName()) + sts, err := a.client.GetStatefulSet(ctx, cr.Namespace, name) + if err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "failed to get sentinel statefulset") + return actor.RequeueWithError(err) + } + } else if sts != nil && sts.Status.ReadyReplicas == *sts.Spec.Replicas { + if _, err := a.client.GetDeployment(ctx, cr.Namespace, name); err != nil { + if !errors.IsNotFound(err) { + return actor.RequeueWithError(err) + } + } else if err := a.client.DeleteDeployment(ctx, cr.Namespace, name); err != nil { + logger.Error(err, "failed to delete old sentinel deployment") + return actor.RequeueWithError(err) + } + } + + // delete sentinel after standalone is ready for old pod to gracefully shutdown + if !inst.IsBindedSentinel() { + var sen v1.RedisSentinel + if err := a.client.Client().Get(ctx, client.ObjectKey{ + Namespace: inst.GetNamespace(), + Name: inst.GetName(), + }, &sen); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "failed to get sentinel statefulset", "sentinel", inst.GetName()) + return actor.RequeueWithError(err) + } else if err == nil { + if err = a.client.Client().Delete(ctx, &sen); err != nil { + logger.Error(err, "failed to delete binded sentinel", "sentinel", inst.GetName()) + return actor.RequeueWithError(err) + } + } + } + } + return nil +} diff --git a/internal/ops/failover/actor/actor_ensure_resource.go b/internal/ops/failover/actor/actor_ensure_resource.go new file mode 100644 index 0000000..e0a4138 --- /dev/null +++ b/internal/ops/failover/actor/actor_ensure_resource.go @@ -0,0 +1,664 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "fmt" + "reflect" + "slices" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + "github.com/alauda/redis-operator/internal/config" + ops "github.com/alauda/redis-operator/internal/ops/failover" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/samber/lo" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ actor.Actor = (*actorEnsureResource)(nil) + +func init() { + actor.Register(core.RedisSentinel, NewEnsureResourceActor) +} + +func NewEnsureResourceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorEnsureResource{ + client: client, + logger: logger, + } +} + +type actorEnsureResource struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorEnsureResource) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandEnsureResource} +} + +func (a *actorEnsureResource) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +// Do +func (a *actorEnsureResource) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandEnsureResource.String()) + + inst := val.(types.RedisFailoverInstance) + if (inst.Definition().Spec.Redis.PodAnnotations != nil) && inst.Definition().Spec.Redis.PodAnnotations[config.PAUSE_ANNOTATION_KEY] != "" { + if ret := a.pauseStatefulSet(ctx, inst, logger); ret != nil { + return ret + } + if ret := a.pauseSentinel(ctx, inst, logger); ret != nil { + return ret + } + return actor.Pause() + } + + if ret := a.ensureRedisSSL(ctx, inst, logger); ret != nil { + return ret + } + if ret := a.ensureServiceAccount(ctx, inst, logger); ret != nil { + return ret + } + if ret := a.ensureSentinel(ctx, inst, logger); ret != nil { + return ret + } + if ret := a.ensureService(ctx, inst, logger); ret != nil { + return ret + } + if ret := a.ensureConfigMap(ctx, inst, logger); ret != nil { + return ret + } + if ret := a.ensureRedisStatefulSet(ctx, inst, logger); ret != nil { + return ret + } + return nil +} + +func (a *actorEnsureResource) ensureRedisStatefulSet(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + var ( + err error + cr = inst.Definition() + selector = inst.Selector() + // isAllACLSupported is used to make sure 5=>6 upgrade can to failover succeed + isAllACLSupported = inst.IsACLAppliedToAll() + ) + + // ensure inst statefulSet + if ret := a.ensurePodDisruptionBudget(ctx, cr, logger, selector); ret != nil { + return ret + } + + sts := failoverbuilder.GenerateRedisStatefulSet(inst, selector, isAllACLSupported) + oldSts, err := a.client.GetStatefulSet(ctx, cr.Namespace, sts.Name) + if errors.IsNotFound(err) { + if err := a.client.CreateStatefulSet(ctx, cr.Namespace, sts); err != nil { + return actor.RequeueWithError(err) + } + return nil + } else if err != nil { + logger.Error(err, "get statefulset failed", "target", client.ObjectKeyFromObject(sts)) + return actor.RequeueWithError(err) + } + + // TODO: remove this and reset rds pvc resize logic + // keep old volumeClaimTemplates + sts.Spec.VolumeClaimTemplates = oldSts.Spec.VolumeClaimTemplates + + if clusterbuilder.IsStatefulsetChanged(sts, oldSts, logger) { + if *oldSts.Spec.Replicas > *sts.Spec.Replicas { + // scale down + oldSts.Spec.Replicas = sts.Spec.Replicas + if err := a.client.UpdateStatefulSet(ctx, cr.Namespace, oldSts); err != nil { + logger.Error(err, "scale down statefulset failed", "target", client.ObjectKeyFromObject(oldSts)) + return actor.RequeueWithError(err) + } + time.Sleep(time.Second * 3) + } + + // patch pods with new labels in selector + pods, err := inst.RawNodes(ctx) + if err != nil { + logger.Error(err, "get pods failed") + return actor.RequeueWithError(err) + } + for _, item := range pods { + pod := item.DeepCopy() + pod.Labels = lo.Assign(pod.Labels, inst.Selector()) + if !reflect.DeepEqual(pod.Labels, item.Labels) { + if err := a.client.UpdatePod(ctx, pod.GetNamespace(), pod); err != nil { + logger.Error(err, "patch pod label failed", "target", client.ObjectKeyFromObject(pod)) + return actor.RequeueWithError(err) + } + } + } + time.Sleep(time.Second * 3) + if err := a.client.DeleteStatefulSet(ctx, cr.Namespace, sts.Name, + client.PropagationPolicy(metav1.DeletePropagationOrphan)); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "delete old statefulset failed", "target", client.ObjectKeyFromObject(sts)) + return actor.RequeueWithError(err) + } + if err = a.client.CreateStatefulSet(ctx, cr.Namespace, sts); err != nil { + logger.Error(err, "update statefulset failed", "target", client.ObjectKeyFromObject(sts)) + return actor.RequeueWithError(err) + } + } + return nil +} + +func (a *actorEnsureResource) ensurePodDisruptionBudget(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { + pdb := failoverbuilder.NewPodDisruptionBudgetForCR(rf, selectors) + + if oldPdb, err := a.client.GetPodDisruptionBudget(context.TODO(), rf.Namespace, pdb.Name); errors.IsNotFound(err) { + if err := a.client.CreatePodDisruptionBudget(ctx, rf.Namespace, pdb); err != nil { + return actor.RequeueWithError(err) + } + } else if err != nil { + return actor.RequeueWithError(err) + } else if !reflect.DeepEqual(oldPdb.Spec.Selector, pdb.Spec.Selector) { + oldPdb.Labels = pdb.Labels + oldPdb.Spec.Selector = pdb.Spec.Selector + if err := a.client.UpdatePodDisruptionBudget(ctx, rf.Namespace, oldPdb); err != nil { + return actor.RequeueWithError(err) + } + } + return nil +} + +func (a *actorEnsureResource) ensureConfigMap(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + cr := inst.Definition() + selector := inst.Selector() + // ensure Redis configMap + if ret := a.ensureRedisConfigMap(ctx, inst, logger, selector); ret != nil { + return ret + } + + if inst.Version().IsACLSupported() { + if ret := failoverbuilder.NewFailoverAclConfigMap(cr, inst.Users().Encode(true)); ret != nil { + if err := a.client.CreateIfNotExistsConfigMap(ctx, cr.Namespace, ret); err != nil { + return actor.RequeueWithError(err) + } + } + } + return nil +} + +func (a *actorEnsureResource) ensureRedisConfigMap(ctx context.Context, st types.RedisFailoverInstance, logger logr.Logger, selectors map[string]string) *actor.ActorResult { + rf := st.Definition() + configMap, err := failoverbuilder.NewRedisConfigMap(st, selectors) + if err != nil { + return actor.RequeueWithError(err) + } + if err := a.client.CreateIfNotExistsConfigMap(ctx, rf.Namespace, configMap); err != nil { + return actor.RequeueWithError(err) + } + return nil +} + +func (a *actorEnsureResource) ensureRedisSSL(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + rf := inst.Definition() + if !rf.Spec.Redis.EnableTLS { + return nil + } + + cert := failoverbuilder.NewCertificate(rf, inst.Selector()) + if err := a.client.CreateIfNotExistsCertificate(ctx, rf.Namespace, cert); err != nil { + return actor.RequeueWithError(err) + } + oldCert, err := a.client.GetCertificate(ctx, rf.Namespace, cert.GetName()) + if err != nil && !errors.IsNotFound(err) { + return actor.RequeueWithError(err) + } + + var ( + secretName = builder.GetRedisSSLSecretName(rf.Name) + secret *corev1.Secret + ) + for i := 0; i < 5; i++ { + if secret, _ = a.client.GetSecret(ctx, rf.Namespace, secretName); secret != nil { + break + } + // check when the certificate created + if time.Since(oldCert.GetCreationTimestamp().Time) > time.Minute*5 { + return actor.NewResultWithError(ops.CommandAbort, fmt.Errorf("issue for tls certificate failed, please check the cert-manager")) + } + time.Sleep(time.Second * time.Duration(i+1)) + } + if secret == nil { + return actor.NewResult(ops.CommandRequeue) + } + return nil +} + +func (a *actorEnsureResource) ensureServiceAccount(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + cr := inst.Definition() + sa := clusterbuilder.NewServiceAccount(cr) + role := clusterbuilder.NewRole(cr) + binding := clusterbuilder.NewRoleBinding(cr) + clusterRole := clusterbuilder.NewClusterRole(cr) + clusterRoleBinding := clusterbuilder.NewClusterRoleBinding(cr) + + if err := a.client.CreateOrUpdateServiceAccount(ctx, inst.GetNamespace(), sa); err != nil { + logger.Error(err, "create service account failed", "target", client.ObjectKeyFromObject(sa)) + return actor.RequeueWithError(err) + } + if err := a.client.CreateOrUpdateRole(ctx, inst.GetNamespace(), role); err != nil { + return actor.RequeueWithError(err) + } + if err := a.client.CreateOrUpdateRoleBinding(ctx, inst.GetNamespace(), binding); err != nil { + return actor.RequeueWithError(err) + } + if err := a.client.CreateOrUpdateClusterRole(ctx, clusterRole); err != nil { + return actor.RequeueWithError(err) + } + if oldClusterRb, err := a.client.GetClusterRoleBinding(ctx, clusterRoleBinding.Name); err != nil { + if errors.IsNotFound(err) { + if err := a.client.CreateClusterRoleBinding(ctx, clusterRoleBinding); err != nil { + return actor.RequeueWithError(err) + } + } else { + return actor.RequeueWithError(err) + } + } else { + exists := false + for _, sub := range oldClusterRb.Subjects { + if sub.Namespace == inst.GetNamespace() { + exists = true + } + } + if !exists && len(oldClusterRb.Subjects) > 0 { + oldClusterRb.Subjects = append(oldClusterRb.Subjects, + rbacv1.Subject{Kind: "ServiceAccount", + Name: clusterbuilder.RedisInstanceServiceAccountName, + Namespace: inst.GetNamespace()}, + ) + err := a.client.CreateOrUpdateClusterRoleBinding(ctx, oldClusterRb) + if err != nil { + return actor.RequeueWithError(err) + } + } + } + return nil +} + +func (a *actorEnsureResource) ensureSentinel(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + if !inst.IsBindedSentinel() { + return nil + } + + { + // COMP: patch labels for old deployment pods + // TODO: remove in 3.22 + selectors := sentinelbuilder.GenerateSelectorLabels(sentinelbuilder.RedisArchRoleSEN, inst.GetName()) + name := sentinelbuilder.GetSentinelStatefulSetName(inst.GetName()) + if ret, err := a.client.GetDeploymentPods(ctx, inst.GetNamespace(), name); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "get sentinel deployment pods failed", "target", util.ObjectKey(inst.GetNamespace(), name)) + return actor.RequeueWithError(err) + } else if ret != nil { + for _, item := range ret.Items { + pod := item.DeepCopy() + pod.Labels = lo.Assign(pod.Labels, selectors) + // not patch sentinel labels + delete(pod.Labels, "redissentinels.databases.spotahome.com/name") + if !reflect.DeepEqual(pod.Labels, item.Labels) { + if err := a.client.UpdatePod(ctx, pod.GetNamespace(), pod); err != nil { + logger.Error(err, "patch sentinel pod label failed", "target", client.ObjectKeyFromObject(pod)) + return actor.RequeueWithError(err) + } + } + } + } + } + + newSen := failoverbuilder.NewFailoverSentinel(inst) + oldSen, err := a.client.GetRedisSentinel(ctx, inst.GetNamespace(), inst.GetName()) + if errors.IsNotFound(err) { + if err := a.client.Client().Create(ctx, newSen); err != nil { + logger.Error(err, "create sentinel failed", "target", client.ObjectKeyFromObject(newSen)) + return actor.RequeueWithError(err) + } + return nil + } else if err != nil { + logger.Error(err, "get sentinel failed", "target", client.ObjectKeyFromObject(newSen)) + return actor.RequeueWithError(err) + } + if !reflect.DeepEqual(newSen.Spec, oldSen.Spec) || + !reflect.DeepEqual(newSen.Labels, oldSen.Labels) || + !reflect.DeepEqual(newSen.Annotations, oldSen.Annotations) { + oldSen.Spec = newSen.Spec + oldSen.Labels = newSen.Labels + oldSen.Annotations = newSen.Annotations + if err := a.client.UpdateRedisSentinel(ctx, oldSen); err != nil { + logger.Error(err, "update sentinel failed", "target", client.ObjectKeyFromObject(oldSen)) + return actor.RequeueWithError(err) + } + } + return nil +} + +func (a *actorEnsureResource) ensureService(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + cr := inst.Definition() + // read write svc + rwSvc := failoverbuilder.NewRWSvcForCR(cr) + roSvc := failoverbuilder.NewReadOnlyForCR(cr) + if err := a.client.CreateOrUpdateIfServiceChanged(ctx, inst.GetNamespace(), rwSvc); err != nil { + return actor.RequeueWithError(err) + } + if err := a.client.CreateOrUpdateIfServiceChanged(ctx, inst.GetNamespace(), roSvc); err != nil { + return actor.RequeueWithError(err) + } + + selector := inst.Selector() + exporterService := failoverbuilder.NewExporterServiceForCR(cr, selector) + if err := a.client.CreateOrUpdateIfServiceChanged(ctx, inst.GetNamespace(), exporterService); err != nil { + return actor.RequeueWithError(err) + } + + if ret := a.cleanUselessService(ctx, cr, logger, selector); ret != nil { + return ret + } + switch cr.Spec.Redis.Expose.ServiceType { + case corev1.ServiceTypeNodePort: + if ret := a.ensureRedisSpecifiedNodePortService(ctx, inst, logger, selector); ret != nil { + return ret + } + case corev1.ServiceTypeLoadBalancer: + if ret := a.ensureRedisPodService(ctx, cr, logger, selector); ret != nil { + return ret + } + } + return nil +} + +func (a *actorEnsureResource) ensureRedisSpecifiedNodePortService(ctx context.Context, + inst types.RedisFailoverInstance, logger logr.Logger, selectors map[string]string) *actor.ActorResult { + cr := inst.Definition() + + if len(cr.Spec.Redis.Expose.NodePortSequence) == 0 { + return a.ensureRedisPodService(ctx, cr, logger, selectors) + } + + logger.V(3).Info("ensure redis cluster nodeports", "namepspace", cr.Namespace, "name", cr.Name) + configedPorts, err := helper.ParseSequencePorts(cr.Spec.Redis.Expose.NodePortSequence) + if err != nil { + return actor.RequeueWithError(err) + } + getClientPort := func(svc *corev1.Service, args ...string) int32 { + name := "client" + if len(args) > 0 { + name = args[0] + } + if port := util.GetServicePortByName(svc, name); port != nil { + return port.NodePort + } + return 0 + } + + serviceNameRange := map[string]struct{}{} + for i := 0; i < int(cr.Spec.Redis.Replicas); i++ { + serviceName := failoverbuilder.GetFailoverNodePortServiceName(cr, i) + serviceNameRange[serviceName] = struct{}{} + } + + // the whole process is divided into 3 steps: + // 1. delete service not in nodeport range + // 2. create new service + // 3. update existing service and restart pod (only one pod is restarted at a same time for each shard) + + // 1. delete service not in nodeport range + // + // when pod not exists and service not in nodeport range, delete service + // NOTE: only delete service whose pod is not found + // let statefulset auto scale up/down for pods + labels := failoverbuilder.GenerateSelectorLabels("redis", cr.Name) + services, ret := a.fetchAllPodBindedServices(ctx, cr.Namespace, labels) + if ret != nil { + return ret + } + for _, svc := range services { + svc := svc.DeepCopy() + occupiedPort := getClientPort(svc) + if _, exists := serviceNameRange[svc.Name]; !exists || !slices.Contains(configedPorts, occupiedPort) { + _, err := a.client.GetPod(ctx, svc.Namespace, svc.Name) + if errors.IsNotFound(err) { + logger.Info("release nodeport service", "service", svc.Name, "port", occupiedPort) + if err = a.client.DeleteService(ctx, svc.Namespace, svc.Name); err != nil { + return actor.RequeueWithError(err) + } + } else if err != nil { + logger.Error(err, "get pods failed", "target", client.ObjectKeyFromObject(svc)) + return actor.RequeueWithError(err) + } + } + } + if services, ret = a.fetchAllPodBindedServices(ctx, cr.Namespace, labels); ret != nil { + return ret + } + + // 2. create new service + var ( + newPorts []int32 + bindedNodeports []int32 + needUpdateServices []*corev1.Service + ) + for _, svc := range services { + svc := svc.DeepCopy() + bindedNodeports = append(bindedNodeports, getClientPort(svc)) + } + // filter used ports + for _, port := range configedPorts { + if !slices.Contains(bindedNodeports, port) { + newPorts = append(newPorts, port) + } + } + for i := 0; i < int(cr.Spec.Redis.Replicas); i++ { + serviceName := failoverbuilder.GetFailoverNodePortServiceName(cr, i) + oldService, err := a.client.GetService(ctx, cr.Namespace, serviceName) + if errors.IsNotFound(err) { + if len(newPorts) == 0 { + continue + } + port := newPorts[0] + svc := failoverbuilder.NewPodNodePortService(cr, i, labels, port) + if err = a.client.CreateService(ctx, svc.Namespace, svc); err != nil { + a.logger.Error(err, "create nodeport service failed", "target", client.ObjectKeyFromObject(svc)) + return actor.NewResultWithValue(ops.CommandRequeue, err) + } + newPorts = newPorts[1:] + continue + } else if err != nil { + return actor.RequeueWithError(err) + } + + svc := failoverbuilder.NewPodNodePortService(cr, i, labels, getClientPort(oldService)) + // check old service for compability + if len(oldService.OwnerReferences) == 0 || + oldService.OwnerReferences[0].Kind == "Pod" || + !reflect.DeepEqual(oldService.Spec, svc.Spec) || + !reflect.DeepEqual(oldService.Labels, svc.Labels) || + !reflect.DeepEqual(oldService.Annotations, svc.Annotations) { + + oldService.OwnerReferences = util.BuildOwnerReferences(cr) + oldService.Spec = svc.Spec + oldService.Labels = svc.Labels + oldService.Annotations = svc.Annotations + if err := a.client.UpdateService(ctx, oldService.Namespace, oldService); err != nil { + a.logger.Error(err, "update nodeport service failed", "target", client.ObjectKeyFromObject(oldService)) + return actor.NewResultWithValue(ops.CommandRequeue, err) + } + } + if port := getClientPort(oldService); port != 0 && !slices.Contains(configedPorts, port) { + needUpdateServices = append(needUpdateServices, oldService) + } + } + + // 3. update existing service and restart pod (only one pod is restarted at a same time for each shard) + if len(needUpdateServices) > 0 && len(newPorts) > 0 { + port, svc := newPorts[0], needUpdateServices[0] + if sp := util.GetServicePortByName(svc, "client"); sp != nil { + sp.NodePort = port + } + + // NOTE: here not make sure the failover success, because the nodeport updated, the communication will be failed + // in k8s, the nodeport can still access for sometime after the nodeport updated + // + // update service + if err = a.client.UpdateService(ctx, svc.Namespace, svc); err != nil { + a.logger.Error(err, "update nodeport service failed", "target", client.ObjectKeyFromObject(svc), "port", port) + return actor.NewResultWithValue(ops.CommandRequeue, err) + } + if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector[builder.PodNameLabelKey]); pod != nil { + if err := a.client.DeletePod(ctx, cr.Namespace, pod.Name); err != nil { + return actor.RequeueWithError(err) + } + return actor.NewResult(ops.CommandRequeue) + } + } + return nil +} + +func (a *actorEnsureResource) ensureRedisPodService(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { + for i := 0; i < int(rf.Spec.Redis.Replicas); i++ { + newSvc := failoverbuilder.NewPodService(rf, i, selectors) + if svc, err := a.client.GetService(ctx, rf.Namespace, newSvc.Name); errors.IsNotFound(err) { + if err = a.client.CreateService(ctx, rf.Namespace, newSvc); err != nil { + logger.Error(err, "create service failed", "target", client.ObjectKeyFromObject(newSvc)) + return actor.RequeueWithError(err) + } + } else if err != nil { + logger.Error(err, "get service failed", "target", client.ObjectKeyFromObject(newSvc)) + return actor.NewResult(ops.CommandRequeue) + } else if newSvc.Spec.Type != svc.Spec.Type || + !reflect.DeepEqual(newSvc.Spec.Selector, svc.Spec.Selector) || + !reflect.DeepEqual(newSvc.Labels, svc.Labels) || + !reflect.DeepEqual(newSvc.Annotations, svc.Annotations) { + svc.Spec = newSvc.Spec + svc.Labels = newSvc.Labels + svc.Annotations = newSvc.Annotations + if err = a.client.UpdateService(ctx, rf.Namespace, svc); err != nil { + logger.Error(err, "update service failed", "target", client.ObjectKeyFromObject(svc)) + return actor.RequeueWithError(err) + } + } + } + return nil +} + +func (a *actorEnsureResource) cleanUselessService(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { + services, err := a.fetchAllPodBindedServices(ctx, rf.Namespace, selectors) + if err != nil { + return err + } + for _, item := range services { + svc := item.DeepCopy() + index, err := util.ParsePodIndex(svc.Name) + if err != nil { + logger.Error(err, "parse svc name failed", "target", client.ObjectKeyFromObject(svc)) + continue + } + if index >= int(rf.Spec.Redis.Replicas) { + _, err := a.client.GetPod(ctx, svc.Namespace, svc.Name) + if errors.IsNotFound(err) { + if err = a.client.DeleteService(ctx, svc.Namespace, svc.Name); err != nil { + return actor.RequeueWithError(err) + } + } else if err != nil { + logger.Error(err, "get pods failed", "target", client.ObjectKeyFromObject(svc)) + return actor.RequeueWithError(err) + } + } + } + return nil +} + +func (a *actorEnsureResource) pauseStatefulSet(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + cr := inst.Definition() + name := failoverbuilder.GetFailoverStatefulSetName(cr.Name) + if sts, err := a.client.GetStatefulSet(ctx, cr.Namespace, name); err != nil { + if errors.IsNotFound(err) { + return nil + } + return actor.RequeueWithError(err) + } else { + if sts.Spec.Replicas == nil || *sts.Spec.Replicas == 0 { + return nil + } + *sts.Spec.Replicas = 0 + if err = a.client.UpdateStatefulSet(ctx, cr.Namespace, sts); err != nil { + return actor.RequeueWithError(err) + } + inst.SendEventf(corev1.EventTypeNormal, config.EventPause, "pause statefulset %s", name) + } + return nil +} + +func (a *actorEnsureResource) pauseSentinel(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + if def := inst.Definition(); def.Spec.Sentinel == nil || + (def.Spec.Sentinel.SentinelReference == nil && def.Spec.Sentinel.Image == "") { + return nil + } + + sen, err := a.client.GetRedisSentinel(ctx, inst.GetNamespace(), inst.GetName()) + if err != nil { + return actor.RequeueWithError(err) + } + if sen.Spec.Replicas == 0 { + return nil + } + sen.Spec.Replicas = 0 + if err := a.client.UpdateRedisSentinel(ctx, sen); err != nil { + logger.Error(err, "pause sentinel failed", "target", client.ObjectKeyFromObject(sen)) + return actor.RequeueWithError(err) + } + inst.SendEventf(corev1.EventTypeNormal, config.EventPause, "pause instance sentinels") + return nil +} + +func (a *actorEnsureResource) fetchAllPodBindedServices(ctx context.Context, namespace string, selector map[string]string) ([]corev1.Service, *actor.ActorResult) { + var services []corev1.Service + if svcRes, err := a.client.GetServiceByLabels(ctx, namespace, selector); err != nil { + return nil, actor.RequeueWithError(err) + } else { + // ignore services without pod selector + for _, svc := range svcRes.Items { + if svc.Spec.Selector[builder.PodNameLabelKey] != "" { + services = append(services, svc) + } + } + } + return services, nil +} diff --git a/internal/ops/failover/actor/actor_heal_monitor.go b/internal/ops/failover/actor/actor_heal_monitor.go new file mode 100644 index 0000000..b389eb5 --- /dev/null +++ b/internal/ops/failover/actor/actor_heal_monitor.go @@ -0,0 +1,305 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "errors" + "fmt" + "net" + "slices" + "strconv" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + ops "github.com/alauda/redis-operator/internal/ops/failover" + "github.com/alauda/redis-operator/internal/redis/failover/monitor" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" +) + +var _ actor.Actor = (*actorHealMaster)(nil) + +func init() { + actor.Register(core.RedisSentinel, NewHealMasterActor) +} + +func NewHealMasterActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorHealMaster{ + client: client, + logger: logger, + } +} + +type actorHealMaster struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorHealMaster) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +func (a *actorHealMaster) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandHealMonitor} +} + +func (a *actorHealMaster) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandHealMonitor.String()) + + inst := val.(types.RedisFailoverInstance) + if len(inst.Nodes()) == 0 { + return actor.NewResult(ops.CommandEnsureResource) + } + + // check current master + var ( + err error + monitorInited bool + instMonitor = inst.Monitor() + masterCandidate redis.RedisNode + monitoringNodes = map[string]struct{}{} + // used to check if all any node online, if not, we should reset the monitor + onlineNodeCount int + // used to indicate whether a node has been registered, if no nodes are registered, + // it means that the node is occupied or the node registration information is wrong and needs to be re-registered; + // in addition, there is an intersection between this check of onlineNodeCount + registeredNodeCount int + ) + + monitorMaster, err := instMonitor.Master(ctx) + if err != nil { + if errors.Is(err, monitor.ErrMultipleMaster) { + // TODO: try fix multiple master + logger.Error(err, "multi masters found, sentinel split brain") + return actor.RequeueWithError(err) + } else if !errors.Is(err, monitor.ErrNoMaster) { + logger.Error(err, "failed to get master node") + return actor.RequeueWithError(err) + } + } else { + monitoringNodes[monitorMaster.Address()] = struct{}{} + if monitor.IsMonitoringNodeOnline(monitorMaster) { + onlineNodeCount += 1 + } + } + + if monitorInited, err = instMonitor.Inited(ctx); err != nil { + logger.Error(err, "failed to check monitor inited") + return actor.RequeueWithError(err) + } + + if replicaNodes, err := instMonitor.Replicas(ctx); err != nil { + logger.Error(err, "failed to get replicas") + return actor.RequeueWithError(err) + } else { + for _, node := range replicaNodes { + monitoringNodes[node.Address()] = struct{}{} + if monitor.IsMonitoringNodeOnline(node) { + onlineNodeCount += 1 + } + } + } + for _, node := range inst.Nodes() { + if !node.IsReady() { + continue + } + addr := net.JoinHostPort(node.DefaultIP().String(), strconv.Itoa(node.Port())) + addr2 := net.JoinHostPort(node.DefaultInternalIP().String(), strconv.Itoa(node.InternalIPort())) + _, ok := monitoringNodes[addr] + _, ok2 := monitoringNodes[addr2] + if ok || ok2 { + registeredNodeCount++ + } + if monitorMaster != nil && (monitorMaster.Address() == addr || monitorMaster.Address() == addr2) { + masterCandidate = node + } + } + + if monitorMaster == nil || !monitorInited || onlineNodeCount == 0 || registeredNodeCount == 0 { + nodes := inst.Nodes() + // cases: + // 1. new create instance, should select one node as master + // 2. sentinel is new created, should check who is the master, or how can do as a master + listeningMasters := map[string]int{} + if masterCandidate == nil { + // check if master exists + for _, node := range nodes { + if node.Role() == core.RedisRoleMaster && node.Info().ConnectedReplicas > 0 { + masterCandidate = node + break + } + } + } + + if masterCandidate == nil { + // check if exists nodes got most replicas + for _, node := range nodes { + if node.Role() == core.RedisRoleReplica && node.IsMasterLinkUp() { + addr := net.JoinHostPort(node.ConfigedMasterIP(), node.ConfigedMasterPort()) + listeningMasters[addr]++ + } + } + masterCandidate = func() redis.RedisNode { + var ( + listeningMostAddr string + count int + ) + for addr, c := range listeningMasters { + if listeningMostAddr == "" { + listeningMostAddr = addr + count = c + continue + } else if c > count { + listeningMostAddr = addr + count = c + } + } + if listeningMostAddr != "" { + for _, node := range nodes { + if net.JoinHostPort(node.ConfigedMasterIP(), node.ConfigedMasterPort()) == listeningMostAddr { + return node + } + } + } + return nil + }() + } + + if masterCandidate == nil { + // find node with most repl offset + replIds := map[string]struct{}{} + for _, node := range nodes { + if node.Info().MasterReplOffset > 0 { + replIds[node.Info().MasterReplId] = struct{}{} + } + } + if len(replIds) == 1 { + slices.SortStableFunc(nodes, func(i, j redis.RedisNode) int { + if i.Info().MasterReplOffset >= j.Info().MasterReplOffset { + return -1 + } + return 1 + }) + masterCandidate = nodes[0] + } + } + + if masterCandidate == nil { + // selected uptime longest node as master + slices.SortStableFunc(nodes, func(i, j redis.RedisNode) int { + if i.Info().UptimeInSeconds >= j.Info().UptimeInSeconds { + return -1 + } + return 1 + }) + masterCandidate = nodes[0] + } + + if masterCandidate != nil { + if !masterCandidate.IsReady() { + logger.Error(fmt.Errorf("candicate master not ready"), "selected master node is not ready", "node", masterCandidate.GetName()) + return actor.Requeue() + } + if err := instMonitor.Monitor(ctx, masterCandidate); err != nil { + logger.Error(err, "failed to init sentinel") + return actor.RequeueWithError(err) + } + addr := net.JoinHostPort(masterCandidate.DefaultIP().String(), strconv.Itoa(masterCandidate.Port())) + logger.Info("setup master node", "addr", addr) + inst.SendEventf(corev1.EventTypeWarning, config.EventSetupMaster, "setup sentinels with master %s", addr) + } else { + err := fmt.Errorf("cannot find any usable master node") + logger.Error(err, "failed to setup master node") + return actor.RequeueWithError(err) + } + } else { + if masterCandidate != nil && masterCandidate.Role() != core.RedisRoleMaster { + // exists cases all nodes are replicas, and sentinel connected to one of this replicas + if err := masterCandidate.Setup(ctx, []any{"REPLICAOF", "NO", "ONE"}); err != nil { + logger.Error(err, "failed to setup replicaof", "node", masterCandidate.GetName()) + return actor.RequeueWithError(err) + } + addr := net.JoinHostPort(masterCandidate.DefaultIP().String(), strconv.Itoa(masterCandidate.Port())) + inst.SendEventf(corev1.EventTypeWarning, config.EventSetupMaster, "reset slave %s node as new master", addr) + return actor.Requeue() + } + + if strings.Contains(monitorMaster.Flags, "down") || masterCandidate == nil { + logger.Info("master node is down, check if it's ok to do MANUAL FAILOVER") + // when master is down, the node many keep healthy in sentinel for about 30s + // here manually check the master node connected status + replicas, err := instMonitor.Replicas(ctx) + if err != nil { + logger.Error(err, "failed to get replicas") + return actor.RequeueWithError(err) + } + healthyReplicas := 0 + for _, repl := range replicas { + if !strings.Contains(repl.Flags, "down") && !strings.Contains(repl.Flags, "disconnected") { + healthyReplicas++ + } + } + if healthyReplicas == 0 { + // TODO: do init setup + err := fmt.Errorf("cannot do failover") + logger.Error(err, "not healthy replicas found for failover") + return actor.NewResultWithError(ops.CommandHealPod, err) + } + logger.Info("master node is down, try FAILOVER MANUALLY") + if err := instMonitor.Failover(ctx); err != nil { + logger.Error(err, "failed to do failover") + return actor.RequeueWithError(err) + } + inst.SendEventf(corev1.EventTypeWarning, config.EventFailover, "try failover as no master found") + return actor.Requeue() + // TODO: maybe we can manually setup the replica as a new master + } else { + masterAddr := monitorMaster.Address() + // check all other nodes connected to master + for _, node := range inst.Nodes() { + if !node.IsReady() { + logger.Error(fmt.Errorf("node is not ready"), "node cannot join cluster", "node", node.GetName()) + continue + } + listeningAddr := net.JoinHostPort(node.DefaultIP().String(), strconv.Itoa(node.Port())) + listeningInternalAddr := net.JoinHostPort(node.DefaultInternalIP().String(), strconv.Itoa(node.InternalIPort())) + if masterAddr == listeningAddr || masterAddr == listeningInternalAddr { + continue + } + + bindedMasterAddr := net.JoinHostPort(node.ConfigedMasterIP(), node.ConfigedMasterPort()) + if bindedMasterAddr == masterAddr && node.IsMasterLinkUp() { + continue + } + + logger.Info("node is not link to master", "node", node.GetName(), "current listening", bindedMasterAddr, "current master", masterAddr) + if err := node.ReplicaOf(ctx, monitorMaster.IP, monitorMaster.Port); err != nil { + logger.Error(err, "failed to rebind replica, rejoin in next reconcile") + continue + } + } + } + } + return nil +} diff --git a/internal/ops/failover/actor/actor_heal_pod.go b/internal/ops/failover/actor/actor_heal_pod.go new file mode 100644 index 0000000..520cf10 --- /dev/null +++ b/internal/ops/failover/actor/actor_heal_pod.go @@ -0,0 +1,160 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + ops "github.com/alauda/redis-operator/internal/ops/failover" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ actor.Actor = (*actorHealPod)(nil) + +func init() { + actor.Register(core.RedisSentinel, NewHealPodActor) +} + +func NewHealPodActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorHealPod{ + client: client, + logger: logger, + } +} + +type actorHealPod struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorHealPod) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandHealPod} +} + +func (a *actorHealPod) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +// Do +func (a *actorHealPod) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandHealPod.String()) + + // clean terminating pods + var ( + inst = val.(types.RedisFailoverInstance) + now = time.Now() + ) + + pods, err := inst.RawNodes(ctx) + if err != nil { + logger.Error(err, "get pods failed") + return actor.RequeueWithError(err) + } + + for _, pod := range pods { + timestamp := pod.GetDeletionTimestamp() + if timestamp == nil { + continue + } + grace := time.Second * 30 + if val := pod.GetDeletionGracePeriodSeconds(); val != nil { + grace = time.Duration(*val) * time.Second + } + if now.Sub(timestamp.Time) <= grace { + continue + } + + objKey := client.ObjectKey{Namespace: pod.GetNamespace(), Name: pod.GetName()} + logger.V(2).Info("for delete pod", "name", pod.GetName()) + // force delete the terminating pods + if err := a.client.DeletePod(ctx, inst.GetNamespace(), pod.GetName(), client.GracePeriodSeconds(0)); err != nil { + logger.Error(err, "force delete pod failed", "target", objKey) + } else { + inst.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, "force delete blocked terminating pod %s", objKey.Name) + logger.Info("force delete blocked terminating pod", "target", objKey) + return actor.Requeue() + } + } + + if typ := inst.Definition().Spec.Redis.Expose.ServiceType; typ == corev1.ServiceTypeNodePort || + typ == corev1.ServiceTypeLoadBalancer { + for _, node := range inst.Nodes() { + if !node.IsReady() { + continue + } + announceIP := node.DefaultIP().String() + announcePort := node.Port() + + svc, err := a.client.GetService(ctx, inst.GetNamespace(), node.GetName()) + if errors.IsNotFound(err) { + logger.Info("service not found", "name", node.GetName()) + return actor.NewResult(ops.CommandEnsureResource) + } else if err != nil { + logger.Error(err, "get service failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } + if typ == corev1.ServiceTypeNodePort { + port := util.GetServicePortByName(svc, "client") + if port != nil { + if int(port.NodePort) != announcePort { + if err := a.client.DeletePod(ctx, inst.GetNamespace(), node.GetName()); err != nil { + logger.Error(err, "delete pod failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } else { + inst.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, + "force delete pod with inconsist annotation %s", node.GetName()) + return actor.Requeue() + } + } + } else { + logger.Error(fmt.Errorf("service port not found"), "service port not found", "name", node.GetName(), "port", "client") + } + } else if typ == corev1.ServiceTypeLoadBalancer { + if index := slices.IndexFunc(svc.Status.LoadBalancer.Ingress, func(ing corev1.LoadBalancerIngress) bool { + return ing.IP == announceIP || ing.Hostname == announceIP + }); index < 0 { + if err := a.client.DeletePod(ctx, inst.GetNamespace(), node.GetName()); err != nil { + logger.Error(err, "delete pod failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } else { + inst.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, + "force delete pod with inconsist annotation %s", node.GetName()) + return actor.Requeue() + } + } + } + } + } + + if fullfilled, _ := inst.IsResourceFullfilled(ctx); !fullfilled { + return actor.NewResult(ops.CommandEnsureResource) + } + return nil +} diff --git a/internal/ops/failover/actor/actor_patch_labels.go b/internal/ops/failover/actor/actor_patch_labels.go new file mode 100644 index 0000000..542a911 --- /dev/null +++ b/internal/ops/failover/actor/actor_patch_labels.go @@ -0,0 +1,127 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "net" + "slices" + "strconv" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + ops "github.com/alauda/redis-operator/internal/ops/failover" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" +) + +var _ actor.Actor = (*actorPatchLabels)(nil) + +func init() { + actor.Register(core.RedisSentinel, NewPatchLabelsActor) +} + +func NewPatchLabelsActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorPatchLabels{ + client: client, + logger: logger, + } +} + +type actorPatchLabels struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorPatchLabels) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +func (a *actorPatchLabels) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandPatchLabels} +} + +func (a *actorPatchLabels) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandPatchLabels.String()) + inst := val.(types.RedisFailoverInstance) + + masterNode, err := inst.Monitor().Master(ctx) + if err != nil { + logger.Error(err, "get master failed") + actor.RequeueWithError(err) + } + + pods, err := inst.RawNodes(ctx) + if err != nil { + logger.Error(err, "get pods failed") + return actor.RequeueWithError(err) + } + masterAddr := net.JoinHostPort(masterNode.IP, masterNode.Port) + + for _, pod := range pods { + var node redis.RedisNode + slices.IndexFunc(inst.Nodes(), func(i redis.RedisNode) bool { + if i.GetName() == pod.GetName() { + node = i + return true + } + return false + }) + + roleLabelVal := pod.GetLabels()[failoverbuilder.RedisRoleLabel] + if node == nil { + if roleLabelVal != "" { + if err := a.client.PatchPodLabel(ctx, pod.DeepCopy(), failoverbuilder.RedisRoleLabel, ""); err != nil { + logger.Error(err, "patch pod label failed") + return actor.RequeueWithError(err) + } + } + continue + } + nodeAddr := net.JoinHostPort(node.DefaultIP().String(), strconv.Itoa(node.Port())) + if node.Role() == core.RedisRoleMaster && nodeAddr == masterAddr { + if roleLabelVal != failoverbuilder.RedisRoleMaster { + err := a.client.PatchPodLabel(ctx, node.Definition(), failoverbuilder.RedisRoleLabel, failoverbuilder.RedisRoleMaster) + if err != nil { + logger.Error(err, "patch pod label failed") + return actor.RequeueWithError(err) + } + } + } else if node.Role() == core.RedisRoleReplica { + if roleLabelVal != failoverbuilder.RedisRoleReplica { + err := a.client.PatchPodLabel(ctx, node.Definition(), failoverbuilder.RedisRoleLabel, failoverbuilder.RedisRoleReplica) + if err != nil { + logger.Error(err, "patch pod label failed") + return actor.RequeueWithError(err) + } + } + } else { + if roleLabelVal != "" { + err := a.client.PatchPodLabel(ctx, node.Definition(), failoverbuilder.RedisRoleLabel, "") + if err != nil { + logger.Error(err, "patch pod label failed") + return actor.RequeueWithError(err) + } + } + } + } + return nil +} diff --git a/internal/ops/failover/actor/actor_update_account.go b/internal/ops/failover/actor/actor_update_account.go new file mode 100644 index 0000000..2e3dbea --- /dev/null +++ b/internal/ops/failover/actor/actor_update_account.go @@ -0,0 +1,343 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "fmt" + "reflect" + "slices" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + midv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + "github.com/alauda/redis-operator/internal/config" + ops "github.com/alauda/redis-operator/internal/ops/failover" + "github.com/alauda/redis-operator/internal/ops/sentinel" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/security/acl" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/pkg/types/user" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ actor.Actor = (*actorUpdateAccount)(nil) + +func init() { + actor.Register(core.RedisSentinel, NewUpdateAccountActor) +} + +func NewUpdateAccountActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorUpdateAccount{ + client: client, + logger: logger, + } +} + +type actorUpdateAccount struct { + client kubernetes.ClientSet + + logger logr.Logger +} + +func (a *actorUpdateAccount) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +// SupportedCommands +func (a *actorUpdateAccount) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandUpdateAccount} +} + +func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandUpdateAccount.String()) + + var ( + inst = val.(types.RedisFailoverInstance) + users = inst.Users() + defaultUser = users.GetDefaultUser() + opUser = users.GetOpUser() + ) + + if defaultUser == nil { + defaultUser, _ = user.NewUser("", user.RoleDeveloper, nil, inst.Version().IsACL2Supported()) + } + + var ( + currentSecretName string = defaultUser.Password.GetSecretName() + newSecretName string + ) + if ps := inst.Definition().Spec.Auth.SecretPath; ps != "" { + newSecretName = ps + } + + isAclEnabled := (opUser.Role == user.RoleOperator) + + name := failoverbuilder.GenerateFailoverACLConfigMapName(inst.GetName()) + oldCm, err := a.client.GetConfigMap(ctx, inst.GetNamespace(), name) + if err != nil && !errors.IsNotFound(err) { + logger.Error(err, "load configmap failed", "target", name) + return actor.NewResultWithError(ops.CommandRequeue, err) + } else if oldCm == nil { + // sync acl configmap + oldCm = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: inst.GetNamespace(), + Labels: inst.GetLabels(), + OwnerReferences: util.BuildOwnerReferences(inst.Definition()), + }, + Data: users.Encode(true), + } + + // create acl with old password + // create redis acl file, after restart, the password is updated + if err := a.client.CreateConfigMap(ctx, inst.GetNamespace(), oldCm); err != nil { + logger.Error(err, "create acl configmap failed", "target", oldCm.Name) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + + // wait for resource sync + time.Sleep(time.Second * 1) + if oldCm, err = a.client.GetConfigMap(ctx, inst.GetNamespace(), name); err != nil { + logger.Error(err, "get configmap failed", "target", name) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + + var newSecret *corev1.Secret + if newSecretName != "" { + if newSecret, err = a.client.GetSecret(ctx, inst.GetNamespace(), newSecretName); errors.IsNotFound(err) { + logger.Error(err, "get sentinel secret failed", "target", newSecretName) + return actor.NewResultWithError(ops.CommandRequeue, fmt.Errorf("secret %s not found", newSecretName)) + } else if err != nil { + logger.Error(err, "get sentinel secret failed", "target", newSecretName) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + + isUpdated := false + if newSecretName != currentSecretName { + defaultUser.Password, _ = user.NewPassword(newSecret) + isUpdated = true + } + users = append(users[0:0], defaultUser) + if inst.Version().IsACLSupported() { + if !isAclEnabled { + secretName := failoverbuilder.GenerateFailoverACLOperatorSecretName(inst.GetName()) + ownRefs := util.BuildOwnerReferences(inst.Definition()) + opUser, err := acl.NewOperatorUser(ctx, a.client, secretName, inst.GetNamespace(), ownRefs, inst.Version().IsACL2Supported()) + if err != nil { + logger.Error(err, "create operator user failed") + return actor.NewResult(ops.CommandRequeue) + } else { + users = append(users, opUser) + isUpdated = true + } + + opRedisUser := failoverbuilder.GenerateFailoverRedisUser(inst, opUser) + if err := a.client.CreateOrUpdateRedisUser(ctx, opRedisUser); err != nil { + logger.Error(err, "create operator redis user failed") + return actor.NewResult(ops.CommandRequeue) + } + inst.SendEventf(corev1.EventTypeNormal, config.EventCreateUser, "created operator user to enable acl") + } else { + if newOpUser, err := acl.NewOperatorUser(ctx, a.client, + opUser.Password.SecretName, inst.GetNamespace(), nil, inst.Version().IsACL2Supported()); err != nil { + logger.Error(err, "create operator user failed") + return actor.NewResult(ops.CommandRequeue) + } else { + opRedisUser := failoverbuilder.GenerateFailoverRedisUser(inst, newOpUser) + if err := a.client.CreateOrUpdateRedisUser(ctx, opRedisUser); err != nil { + logger.Error(err, "update operator redis user failed") + return actor.NewResult(ops.CommandRequeue) + } + inst.SendEventf(corev1.EventTypeNormal, config.EventCreateUser, "created/updated operator user") + + opUser.Rules = newOpUser.Rules + users = append(users, opUser) + + isUpdated = true + } + } + + defaultRedisUser := failoverbuilder.GenerateFailoverRedisUser(inst, defaultUser) + defaultRedisUser.Annotations[midv1.ACLSupportedVersionAnnotationKey] = inst.Version().String() + if oldDefaultRU, err := a.client.GetRedisUser(ctx, inst.GetNamespace(), defaultRedisUser.GetName()); errors.IsNotFound(err) { + if err := a.client.CreateIfNotExistsRedisUser(ctx, defaultRedisUser); err != nil { + logger.Error(err, "update default redis user failed") + return actor.NewResult(ops.CommandRequeue) + } + inst.SendEventf(corev1.EventTypeNormal, config.EventCreateUser, "created default user") + } else if err != nil { + logger.Error(err, "get default redisuser failed") + return actor.NewResultWithError(ops.CommandRequeue, err) + } else if inst.Version().IsACL2Supported() { + oldVersion := redis.RedisVersion(oldDefaultRU.Annotations[midv1.ACLSupportedVersionAnnotationKey]) + // COMP: if old version not support acl2, and new version is supported, update acl rules for compatibility + if !oldVersion.IsACL2Supported() { + fields := strings.Fields(oldDefaultRU.Spec.AclRules) + if !slices.Contains(fields, "&*") && !slices.Contains(fields, "allchannels") { + oldDefaultRU.Spec.AclRules = fmt.Sprintf("%s &*", oldDefaultRU.Spec.AclRules) + } + if oldDefaultRU.Annotations == nil { + oldDefaultRU.Annotations = make(map[string]string) + } + oldDefaultRU.Annotations[midv1.ACLSupportedVersionAnnotationKey] = inst.Version().String() + if err := a.client.UpdateRedisUser(ctx, oldDefaultRU); err != nil { + logger.Error(err, "update default redis user failed") + return actor.NewResult(ops.CommandRequeue) + } + inst.SendEventf(corev1.EventTypeNormal, config.EventUpdateUser, "migrate default user acl rules to support channels") + } + } + } + + if !reflect.DeepEqual(users.Encode(true), oldCm.Data) { + isUpdated = true + } + for k, v := range users.Encode(true) { + oldCm.Data[k] = v + } + if isUpdated { + if err := a.client.CreateOrUpdateConfigMap(ctx, inst.GetNamespace(), oldCm); err != nil { + logger.Error(err, "update acl configmap failed", "target", oldCm.Name) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + if err := inst.Refresh(ctx); err != nil { + logger.Error(err, "refresh resource failed") + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + + var ( + isACLAppliedInPods = true + isAllACLSupported = true + isAllPodReady = true + ) + for _, node := range inst.Nodes() { + if !node.CurrentVersion().IsACLSupported() { + isAllACLSupported = false + break + } + // check if acl have been applied to container + if !node.IsACLApplied() { + isACLAppliedInPods = false + } + if node.ContainerStatus() == nil || !node.ContainerStatus().Ready || + node.IsTerminating() { + isAllPodReady = false + } + } + + logger.V(3).Info("update account", + "isAllACLSupported", isAllACLSupported, + "isACLAppliedInPods", isACLAppliedInPods, + "version", inst.Users().Encode(true), + ) + + if inst.Version().IsACLSupported() { + if isAllACLSupported { + if !isACLAppliedInPods && isAllPodReady { + margs := [][]interface{}{} + margs = append( + margs, + []interface{}{"config", "set", "masteruser", inst.Users().GetOpUser().Name}, + []interface{}{"config", "set", "masterauth", inst.Users().GetOpUser().Password}, + ) + for _, node := range inst.Nodes() { + if node.ContainerStatus() == nil || !node.IsReady() || node.IsTerminating() { + continue + } + if err := node.Setup(ctx, margs...); err != nil { + logger.Error(err, "update acl config failed") + } + } + inst.SendEventf(corev1.EventTypeNormal, config.EventUpdatePassword, "updated instance password and injected acl users") + } + } + } else { + var ( + password = "" + secret *corev1.Secret + ) + if passwordSecret := inst.Definition().Spec.Auth.SecretPath; passwordSecret != "" { + if secret, err = a.client.GetSecret(ctx, inst.GetNamespace(), passwordSecret); err == nil { + password = string(secret.Data["password"]) + } else if !errors.IsNotFound(err) { + return actor.NewResultWithError(sentinel.CommandRequeue, err) + } + } + + updateMasterAuth := []interface{}{"config", "set", "masterauth", password} + updateRequirePass := []interface{}{"config", "set", "requirepass", password} + // 如果全部节点更新密码,设置sentinel的密码 + allRedisNodeApplied := true + for _, node := range inst.Nodes() { + if node.ContainerStatus() == nil || !node.IsReady() || node.IsTerminating() { + allRedisNodeApplied = false + continue + } + if err := node.Setup(ctx, updateMasterAuth, updateRequirePass); err != nil { + allRedisNodeApplied = false + logger.Error(err, "update nodes auth info failed") + } + + // Retry hard + cmd := []string{"sh", "-c", fmt.Sprintf(`echo -n '%s' > /tmp/newpass`, password)} + if err := util.RetryOnTimeout(func() error { + _, _, err := a.client.Exec(ctx, node.GetNamespace(), node.GetName(), failoverbuilder.ServerContainerName, cmd) + return err + }, 5); err != nil { + logger.Error(err, "patch new secret to pod failed", "pod", node.GetName()) + } + } + if allRedisNodeApplied { + if err := inst.Monitor().UpdateConfig(ctx, map[string]string{"auth-pass": password}); err != nil { + logger.Error(err, "update sentinel auth info failed") + } + } + users := inst.Users() + if secret != nil { + passwd, err := user.NewPassword(secret) + if err != nil { + return actor.NewResultWithError(sentinel.CommandRequeue, err) + } + users.GetDefaultUser().Password = passwd + } else { + users.GetDefaultUser().Password = nil + } + data := users.Encode(true) + err := a.client.CreateOrUpdateConfigMap(ctx, inst.GetNamespace(), failoverbuilder.NewFailoverAclConfigMap(inst.Definition(), data)) + if err != nil { + return actor.NewResultWithError(sentinel.CommandRequeue, err) + } + inst.SendEventf(corev1.EventTypeNormal, config.EventUpdatePassword, "updated instance password") + + return actor.NewResult(sentinel.CommandRequeue) + } + return nil +} diff --git a/pkg/ops/sentinel/actor/actor_update_config.go b/internal/ops/failover/actor/actor_update_config.go similarity index 68% rename from pkg/ops/sentinel/actor/actor_update_config.go rename to internal/ops/failover/actor/actor_update_config.go index c2c60cc..d163cd2 100644 --- a/pkg/ops/sentinel/actor/actor_update_config.go +++ b/internal/ops/failover/actor/actor_update_config.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,11 +20,13 @@ import ( "context" "fmt" + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + ops "github.com/alauda/redis-operator/internal/ops/failover" "github.com/alauda/redis-operator/pkg/actor" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/ops/sentinel" "github.com/alauda/redis-operator/pkg/types" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/errors" @@ -33,6 +35,10 @@ import ( var _ actor.Actor = (*actorUpdateConfigMap)(nil) +func init() { + actor.Register(core.RedisSentinel, NewSentinelUpdateConfig) +} + func NewSentinelUpdateConfig(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { return &actorUpdateConfigMap{ client: client, @@ -46,18 +52,27 @@ type actorUpdateConfigMap struct { } func (a *actorUpdateConfigMap) SupportedCommands() []actor.Command { - return []actor.Command{sentinel.CommandUpdateConfig} + return []actor.Command{ops.CommandUpdateConfig} +} + +func (a *actorUpdateConfigMap) Version() *semver.Version { + return semver.MustParse("3.18.0") } func (a *actorUpdateConfigMap) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandUpdateConfig.String()) + st := val.(types.RedisFailoverInstance) selectors := st.Selector() - newCm := sentinelbuilder.NewRedisConfigMap(st, selectors) + newCm, err := failoverbuilder.NewRedisConfigMap(st, selectors) + if err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } oldCm, err := a.client.GetConfigMap(ctx, newCm.GetNamespace(), newCm.GetName()) if errors.IsNotFound(err) || oldCm.Data[clusterbuilder.RedisConfKey] == "" { - return actor.NewResultWithError(sentinel.CommandEnsureResource, fmt.Errorf("configmap %s not found", newCm.GetName())) + return actor.NewResultWithError(ops.CommandEnsureResource, fmt.Errorf("configmap %s not found", newCm.GetName())) } else if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) + return actor.NewResultWithError(ops.CommandRequeue, err) } newConf, _ := clusterbuilder.LoadRedisConfig(newCm.Data[clusterbuilder.RedisConfKey]) oldConf, _ := clusterbuilder.LoadRedisConfig(oldCm.Data[clusterbuilder.RedisConfKey]) @@ -65,8 +80,8 @@ func (a *actorUpdateConfigMap) Do(ctx context.Context, val types.RedisInstance) if len(deleted) > 0 || len(added) > 0 || len(changed) > 0 { // NOTE: update configmap first may cause the hot config fail for it will not retry again if err := a.client.UpdateConfigMap(ctx, newCm.GetNamespace(), newCm); err != nil { - a.logger.Error(err, "update config failed", "target", client.ObjectKeyFromObject(newCm)) - return actor.NewResultWithError(sentinel.CommandRequeue, err) + logger.Error(err, "update config failed", "target", client.ObjectKeyFromObject(newCm)) + return actor.NewResultWithError(ops.CommandRequeue, err) } } for k, v := range added { @@ -82,13 +97,13 @@ func (a *actorUpdateConfigMap) Do(ctx context.Context, val types.RedisInstance) if foundRestartApplyConfig { err := st.Restart(ctx) if err != nil { - a.logger.Error(err, "restart redis failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) + logger.Error(err, "restart redis failed") + return actor.NewResultWithError(ops.CommandRequeue, err) } } else { var margs [][]interface{} for key, vals := range changed { - a.logger.V(2).Info("hot config ", "key", key, "value", vals.String()) + logger.V(2).Info("hot config ", "key", key, "value", vals.String()) margs = append(margs, []interface{}{"config", "set", key, vals.String()}) } var ( @@ -107,10 +122,8 @@ func (a *actorUpdateConfigMap) Do(ctx context.Context, val types.RedisInstance) } if !isUpdateFailed { - return actor.NewResultWithError(sentinel.CommandRequeue, err) + return actor.NewResultWithError(ops.CommandRequeue, err) } - } return nil - } diff --git a/internal/ops/failover/command.go b/internal/ops/failover/command.go new file mode 100644 index 0000000..4758907 --- /dev/null +++ b/internal/ops/failover/command.go @@ -0,0 +1,41 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failover + +import ( + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/pkg/actor" +) + +var ( + CommandRequeue = actor.CommandRequeue + CommandAbort = actor.CommandAbort + CommandPaused = actor.CommandPaused + + CommandUpdateAccount = actor.NewCommand(core.RedisSentinel, "CommandUpdateAccount") + CommandUpdateConfig = actor.NewCommand(core.RedisSentinel, "CommandUpdateConfig") + CommandEnsureResource = actor.NewCommand(core.RedisSentinel, "CommandEnsureResource") + CommandHealPod = actor.NewCommand(core.RedisSentinel, "CommandHealPod") + CommandHealMonitor = actor.NewCommand(core.RedisSentinel, "CommandHealMonitor") + CommandPatchLabels = actor.NewCommand(core.RedisSentinel, "CommandPatchLabels") + CommandCleanResource = actor.NewCommand(core.RedisSentinel, "CommandCleanResource") + + // 3.16 compatibility + CommandInitMaster = actor.NewCommand(core.RedisSentinel, "CommandInitMaster") + CommandHealMaster = actor.NewCommand(core.RedisSentinel, "CommandHealMaster") + CommandSentinelHeal = actor.NewCommand(core.RedisSentinel, "CommandSentinelHeal") +) diff --git a/internal/ops/failover/engine.go b/internal/ops/failover/engine.go new file mode 100644 index 0000000..b9da14c --- /dev/null +++ b/internal/ops/failover/engine.go @@ -0,0 +1,403 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failover + +import ( + "context" + "fmt" + "net" + "reflect" + "slices" + "strconv" + "time" + + "github.com/alauda/redis-operator/api/core" + v1 "github.com/alauda/redis-operator/api/databases/v1" + midv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/redis/failover/monitor" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/security/acl" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/pkg/types/user" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/record" +) + +type RuleEngine struct { + client kubernetes.ClientSet + eventRecorder record.EventRecorder + logger logr.Logger +} + +func NewRuleEngine(client kubernetes.ClientSet, eventRecorder record.EventRecorder, logger logr.Logger) (*RuleEngine, error) { + if client == nil { + return nil, fmt.Errorf("require client set") + } + if eventRecorder == nil { + return nil, fmt.Errorf("require EventRecorder") + } + + ctrl := RuleEngine{ + client: client, + eventRecorder: eventRecorder, + logger: logger, + } + return &ctrl, nil +} + +func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger() + + logger.V(3).Info("Inspecting redis failover") + inst := val.(types.RedisFailoverInstance) + if inst == nil { + return nil + } + cr := inst.Definition() + + if (cr.Spec.Redis.PodAnnotations != nil) && cr.Spec.Redis.PodAnnotations[config.PAUSE_ANNOTATION_KEY] != "" { + return actor.NewResult(CommandEnsureResource) + } + + // NOTE: checked if resource is fullfilled + if isFullfilled, _ := inst.IsResourceFullfilled(ctx); !isFullfilled { + return actor.NewResult(CommandEnsureResource) + } + + // check password + if ret := g.isPasswordChanged(ctx, inst, logger); ret != nil { + logger.V(3).Info("checked password", "result", ret) + return ret + } + + // check configmap + if ret := g.isConfigChanged(ctx, inst, logger); ret != nil { + logger.V(3).Info("checked config", "result", ret) + return ret + } + + // check master + if ret := g.isNodesHealthy(ctx, inst, logger); ret != nil { + logger.V(3).Info("checked nodes healthy", "result", ret) + return ret + } + + // ensure rw service + if ret := g.isPatchLabelNeeded(ctx, inst, logger); ret != nil { + logger.V(3).Info("checked labels", "result", ret) + return ret + } + + // do clean check + if ret := g.isResourceCleanNeeded(ctx, inst, logger); ret != nil { + logger.V(3).Info("clean useless resources", "result", ret) + return ret + } + return actor.NewResult(CommandEnsureResource) +} + +func (g *RuleEngine) isPatchLabelNeeded(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + if len(inst.Masters()) != 1 { + return nil + } + + masterNode, err := inst.Monitor().Master(ctx) + if err != nil { + logger.Error(err, "failed to get master node") + return actor.RequeueWithError(err) + } + masterAddr := net.JoinHostPort(masterNode.IP, masterNode.Port) + + pods, err := inst.RawNodes(ctx) + if err != nil { + return actor.RequeueWithError(err) + } + + for _, pod := range pods { + var node redis.RedisNode + slices.IndexFunc(inst.Nodes(), func(i redis.RedisNode) bool { + if i.GetName() == pod.GetName() { + node = i + return true + } + return false + }) + + labels := pod.GetLabels() + labelVal := labels[failoverbuilder.RedisRoleLabel] + if node == nil { + if labelVal != "" { + logger.V(3).Info("node not accessable", "name", pod.GetName(), "labels", labels) + return actor.NewResult(CommandPatchLabels) + } + continue + } + + nodeAddr := net.JoinHostPort(node.DefaultIP().String(), strconv.Itoa(node.Port())) + switch { + case nodeAddr == masterAddr && labelVal != failoverbuilder.RedisRoleMaster: + fallthrough + case labelVal == failoverbuilder.RedisRoleMaster && nodeAddr != masterAddr: + fallthrough + case node.Role() == core.RedisRoleReplica && labelVal != failoverbuilder.RedisRoleReplica: + logger.V(3).Info("master labels not match", "node", node.GetName(), "labels", labels) + return actor.NewResult(CommandPatchLabels) + } + } + return nil +} + +func (g *RuleEngine) isPasswordChanged(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + logger.V(3).Info("checkPassword") + + var ( + cr = inst.Definition() + currentSecretName string + users = inst.Users() + ) + + if passwordSecret := cr.Spec.Auth.SecretPath; passwordSecret != "" { + currentSecretName = passwordSecret + } + defaultUser := users.GetDefaultUser() + // UPGRADE: use RedisUser controller to manage the user update + if defaultUser.GetPassword().GetSecretName() != currentSecretName && !inst.Version().IsACLSupported() { + return actor.NewResult(CommandUpdateAccount) + } + + name := failoverbuilder.GenerateFailoverACLConfigMapName(inst.GetName()) + if _, err := g.client.GetConfigMap(ctx, inst.GetNamespace(), name); err != nil && !errors.IsNotFound(err) { + return actor.RequeueWithError(err) + } else if errors.IsNotFound(err) { + return actor.NewResult(CommandUpdateAccount) + } + + isAclEnabled := (users.GetOpUser().Role == user.RoleOperator) + if inst.Version().IsACLSupported() && !isAclEnabled { + return actor.NewResult(CommandUpdateAccount) + } + if inst.Version().IsACLSupported() && !inst.IsACLUserExists() { + logger.Info("acl user not exists") + return actor.NewResult(CommandUpdateAccount) + } + + cmName := failoverbuilder.GenerateFailoverACLConfigMapName(inst.GetName()) + if cm, err := g.client.GetConfigMap(ctx, inst.GetNamespace(), cmName); errors.IsNotFound(err) { + return actor.NewResult(CommandUpdateAccount) + } else if err != nil { + logger.Error(err, "failed to get configmap", "configmap", cmName) + return actor.NewResult(CommandRequeue) + } else if users, err := acl.LoadACLUsers(ctx, g.client, cm); err != nil { + return actor.NewResult(CommandUpdateAccount) + } else { + if inst.Version().IsACL2Supported() { + opUser := users.GetOpUser() + logger.V(3).Info("check acl2 support", "role", opUser.Role, "rules", opUser.Rules) + if opUser.Role == user.RoleOperator && (len(opUser.Rules) == 0 || len(opUser.Rules[0].Channels) == 0) { + logger.Info("operator user has no channel rules") + return actor.NewResult(CommandUpdateAccount) + } + + defaultRUName := failoverbuilder.GenerateFailoverRedisUserName(inst.GetName(), defaultUser.Name) + if defaultRU, err := g.client.GetRedisUser(ctx, inst.GetNamespace(), defaultRUName); errors.IsNotFound(err) { + return actor.NewResult(CommandUpdateAccount) + } else if err != nil { + return actor.RequeueWithError(err) + } else { + oldVersion := redis.RedisVersion(defaultRU.Annotations[midv1.ACLSupportedVersionAnnotationKey]) + if !oldVersion.IsACL2Supported() { + return actor.NewResult(CommandUpdateAccount) + } + } + } + if !reflect.DeepEqual(users.Encode(true), users.Encode(false)) { + return actor.NewResult(CommandUpdateAccount) + } + } + return nil +} + +func (g *RuleEngine) isConfigChanged(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + newCm, err := failoverbuilder.NewRedisConfigMap(inst, inst.Selector()) + if err != nil { + return actor.RequeueWithError(err) + } + oldCm, err := g.client.GetConfigMap(ctx, newCm.GetNamespace(), newCm.GetName()) + if errors.IsNotFound(err) || oldCm.Data[clusterbuilder.RedisConfKey] == "" { + err := fmt.Errorf("configmap %s not found", newCm.GetName()) + return actor.NewResultWithError(CommandEnsureResource, err) + } else if err != nil { + return actor.RequeueWithError(err) + } + newConf, _ := clusterbuilder.LoadRedisConfig(newCm.Data[clusterbuilder.RedisConfKey]) + oldConf, _ := clusterbuilder.LoadRedisConfig(oldCm.Data[clusterbuilder.RedisConfKey]) + added, changed, deleted := oldConf.Diff(newConf) + if len(added)+len(changed)+len(deleted) != 0 { + return actor.NewResult(CommandUpdateConfig) + } + + if inst.Monitor().Policy() == v1.SentinelFailoverPolicy { + // HACK: check and update sentinel monitor config + // here check and updated sentinel monitor config directly + if err := inst.Monitor().UpdateConfig(ctx, inst.Definition().Spec.Sentinel.MonitorConfig); err != nil { + logger.Error(err, "failed to update sentinel monitor config") + return actor.RequeueWithError(err) + } + } + return nil +} + +// 检查是否有节点未加入集群 +func (g *RuleEngine) isNodesHealthy(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + // check if svc and pod in consistence + for _, node := range inst.Nodes() { + if typ := inst.Definition().Spec.Redis.Expose.ServiceType; node.IsReady() && + (typ == corev1.ServiceTypeNodePort || typ == corev1.ServiceTypeLoadBalancer) { + announceIP := node.DefaultIP().String() + announcePort := node.Port() + svc, err := g.client.GetService(ctx, inst.GetNamespace(), node.GetName()) + if errors.IsNotFound(err) { + return actor.NewResult(CommandEnsureResource) + } else if err != nil { + return actor.RequeueWithError(err) + } + if typ == corev1.ServiceTypeNodePort { + port := util.GetServicePortByName(svc, "client") + if port != nil { + if int(port.NodePort) != announcePort { + return actor.NewResult(CommandHealPod) + } + } else { + logger.Error(fmt.Errorf("service %s not found", node.GetName()), "failed to get service, which should not happen") + } + } else if typ == corev1.ServiceTypeLoadBalancer { + if slices.IndexFunc(svc.Status.LoadBalancer.Ingress, func(i corev1.LoadBalancerIngress) bool { + return i.IP == announceIP + }) < 0 { + return actor.NewResult(CommandHealPod) + } + } + } + } + + // AllNodeMonitored can check if master is online. + // If master is down on any node, trigger CommandHealMonitor + allMonitored, err := inst.Monitor().AllNodeMonitored(ctx) + if err != nil { + if err == monitor.ErrMultipleMaster { + logger.Error(err, "multi master found") + return actor.NewResult(CommandHealMonitor) + } + logger.Error(err, "failed to check all nodes monitored") + return actor.RequeueWithError(err) + } + if !allMonitored { + logger.Info("not all nodes monitored") + return actor.NewResult(CommandHealMonitor) + } + + monitorMaster, err := inst.Monitor().Master(ctx) + if err == monitor.ErrMultipleMaster || err == monitor.ErrNoMaster { + logger.Error(err, "no usable master nodes found") + return actor.NewResult(CommandHealMonitor) + } else if err != nil { + logger.Error(err, "failed to get master") + return actor.RequeueWithError(err) + } + + var masterNode redis.RedisNode + for _, node := range inst.Nodes() { + addr := net.JoinHostPort(node.DefaultIP().String(), strconv.Itoa(node.Port())) + addr2 := net.JoinHostPort(node.DefaultInternalIP().String(), strconv.Itoa(node.InternalPort())) + if addr == monitorMaster.Address() || addr2 == monitorMaster.Address() { + masterNode = node + break + } + } + // TODO: here need more check of node connection + if masterNode == nil { + logger.Info("master not found on any nodes, maybe master is down") + return actor.NewResult(CommandHealMonitor) + } + + now := time.Now() + for _, node := range inst.Nodes() { + if node.IsTerminating() && + now.After(node.GetDeletionTimestamp(). + Add(time.Duration(*node.GetDeletionGracePeriodSeconds())*time.Second)) { + logger.Info("redis node terminted", "node", node.GetName()) + return actor.NewResult(CommandHealPod) + } + } + + for i, node := range inst.Nodes() { + if i != node.Index() { + logger.Info("redis node index not match", "node", node.GetName(), "index", node.Index()) + return actor.NewResult(CommandHealPod) + } + } + return nil +} + +// 最后比对数量是否相同 +func (g *RuleEngine) isResourceCleanNeeded(ctx context.Context, inst types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { + if inst.IsReady() { + // delete old deployment + // TODO: remove in 3.22 + name := failoverbuilder.GetFailoverDeploymentName(inst.GetName()) + sts, err := g.client.GetStatefulSet(ctx, inst.GetNamespace(), name) + if err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "failed to get sentinel statefulset") + return actor.RequeueWithError(err) + } + } else if sts != nil && sts.Status.ReadyReplicas == *sts.Spec.Replicas { + if _, err := g.client.GetDeployment(ctx, inst.GetNamespace(), name); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "failed to get deployment", "deployment", name) + return actor.RequeueWithError(err) + } else if err == nil { + return actor.NewResult(CommandCleanResource) + } + } + + // delete sentinel after standalone is ready for old pod to gracefully shutdown + if !inst.IsBindedSentinel() { + var sen v1.RedisSentinel + if err := g.client.Client().Get(ctx, client.ObjectKey{ + Namespace: inst.GetNamespace(), + Name: inst.GetName(), + }, &sen); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "failed to get sentinel statefulset", "sentinel", inst.GetName()) + return actor.RequeueWithError(err) + } else if err == nil { + return actor.NewResult(CommandCleanResource) + } + } + // TODO: clean standalone ha configmap when switch arch from standalone to failover + } + return nil +} diff --git a/internal/ops/ops.go b/internal/ops/ops.go new file mode 100644 index 0000000..ca625b6 --- /dev/null +++ b/internal/ops/ops.go @@ -0,0 +1,263 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ops + +import ( + "context" + "fmt" + "runtime/debug" + "time" + + "github.com/alauda/redis-operator/api/cluster/v1alpha1" + v1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/ops/cluster" + "github.com/alauda/redis-operator/internal/ops/failover" + "github.com/alauda/redis-operator/internal/ops/sentinel" + clustermodel "github.com/alauda/redis-operator/internal/redis/cluster" + failovermodel "github.com/alauda/redis-operator/internal/redis/failover" + sentinelmodel "github.com/alauda/redis-operator/internal/redis/sentinel" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/kubernetes/clientset" + "github.com/alauda/redis-operator/pkg/types" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + MaxCallDepth = 15 + DefaultRequeueDuration = time.Second * 15 + DefaultAbortRequeueDuration = -1 + PauseRequeueDuration = -1 + DefaultReconcileTimeout = time.Minute * 5 +) + +type contextDepthKey struct{} +type contextLastCommandKey struct{} + +var ( + DepthKey = contextDepthKey{} + LastCommandKey = contextLastCommandKey{} +) + +type RuleEngine interface { + Inspect(ctx context.Context, instance types.RedisInstance) *actor.ActorResult +} + +// OpEngine +type OpEngine struct { + client kubernetes.ClientSet + eventRecorder record.EventRecorder + + clusterRuleEngine RuleEngine + failoverRuleEngine RuleEngine + sentinelRuleEngine RuleEngine + actorManager *actor.ActorManager + + logger logr.Logger +} + +// NewOpEngine +func NewOpEngine(client client.Client, eventRecorder record.EventRecorder, manager *actor.ActorManager, logger logr.Logger) (*OpEngine, error) { + if client == nil { + return nil, fmt.Errorf("require k8s clientset") + } + if eventRecorder == nil { + return nil, fmt.Errorf("require k8s event recorder") + } + if manager == nil { + return nil, fmt.Errorf("require actor manager") + } + + e := OpEngine{ + client: clientset.New(client, logger), + eventRecorder: eventRecorder, + actorManager: manager, + // logger: logger.WithName("OpEngine"), + logger: logger, + } + + if engine, err := cluster.NewRuleEngine(e.client, eventRecorder, logger.WithName("ClusterOpEngine")); err != nil { + return nil, err + } else { + e.clusterRuleEngine = engine + } + + if engine, err := failover.NewRuleEngine(e.client, eventRecorder, logger.WithName("FailoverOpEngine")); err != nil { + return nil, err + } else { + e.failoverRuleEngine = engine + } + + if engine, err := sentinel.NewRuleEngine(e.client, eventRecorder, logger.WithName("SentinelOpEngine")); err != nil { + return nil, err + } else { + e.sentinelRuleEngine = engine + } + return &e, nil +} + +// Run +func (e *OpEngine) Run(ctx context.Context, val client.Object) (ctrl.Result, error) { + logger := e.logger.WithName("OpEngine").WithValues("target", client.ObjectKeyFromObject(val)) + + // each reconcile max run 5 minutes + ctx, cancel := context.WithTimeout(ctx, DefaultReconcileTimeout) + defer cancel() + + engine, inst, err := e.loadEngInst(ctx, val) + if err != nil { + logger.Error(err, "load instance failed") + return ctrl.Result{}, err + } + return e.reconcile(ctx, engine, inst) +} + +func (e *OpEngine) reconcile(ctx context.Context, engine RuleEngine, val types.RedisInstance) (requeue ctrl.Result, err error) { + logger := val.Logger() + + defer func() { + if e := recover(); e != nil { + logger.Error(fmt.Errorf("%s", e), "reconcile panic") + err = fmt.Errorf("%s", e) + debug.PrintStack() + } + }() + + var ( + failMsg string + status = types.Any + ret = engine.Inspect(ctx, val) + lastCommand actor.Command + requeueDuration time.Duration + ) + +__end__: + for depth := 0; ret != nil && depth <= MaxCallDepth; depth += 1 { + if depth == MaxCallDepth { + // use depth to limit the call black hole + logger.Info(fmt.Sprintf("reconcile call depth exceeds %d, force requeue", MaxCallDepth), "target", val.GetName()) + + requeueDuration = time.Second * 30 + break __end__ + } + msg := fmt.Sprintf("run %s", ret.NextCommand().String()) + if lastCommand != nil { + msg = fmt.Sprintf("run %s, last %s", ret.NextCommand().String(), lastCommand.String()) + } + logger.Info(msg, "depth", depth) + + switch ret.NextCommand() { + case actor.CommandAbort: + requeueDuration, _ = ret.Result().(time.Duration) + status, failMsg = types.Fail, "" + if err := ret.Err(); err != nil { + logger.Error(err, "reconcile aborted", "target", val.GetName()) + failMsg = err.Error() + } else { + logger.Info("reconcile aborted", "target", val.GetName()) + } + if requeueDuration == 0 { + requeueDuration = DefaultAbortRequeueDuration + } + break __end__ + case actor.CommandRequeue: + requeueDuration, _ = ret.Result().(time.Duration) + status, failMsg = types.Any, "" + if err := ret.Err(); err != nil { + logger.Error(err, "requeue with error", "target", val.GetName()) + failMsg = err.Error() + } else { + logger.V(3).Info("reconcile requeue", "target", val.GetName()) + } + break __end__ + case actor.CommandPaused: + requeueDuration, _ = ret.Result().(time.Duration) + status, failMsg = types.Paused, "" + if err := ret.Err(); err != nil { + logger.Error(err, "pause failed with error", "target", val.GetName()) + failMsg = err.Error() + } else { + logger.V(3).Info("pause with no error", "target", val.GetName()) + } + if requeueDuration == 0 { + requeueDuration = PauseRequeueDuration + } + break __end__ + } + + ctx = context.WithValue(ctx, DepthKey, depth) + if lastCommand != nil { + ctx = context.WithValue(ctx, LastCommandKey, lastCommand.String()) + } + + ac := e.actorManager.Search(ret.NextCommand(), val) + if ac == nil { + err := fmt.Errorf("unknown command %s", ret.NextCommand()) + logger.Error(err, "actor for command not register") + return ctrl.Result{}, err + } + lastCommand = ret.NextCommand() + logger.V(3).Info(fmt.Sprintf("found actor %s with version %s", ret.NextCommand(), ac.Version())) + + if ret = ac.Do(ctx, val); ret == nil { + // Re-inspect the instance to end the loop + logger.V(3).Info("actor return nil, inspect one more time") + ret = engine.Inspect(ctx, val) + } + if ret != nil && lastCommand != nil && ret.NextCommand().String() == lastCommand.String() { + ret = actor.Requeue() + } + } + + if err := val.UpdateStatus(ctx, status, failMsg); err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + logger.Error(err, "update status before requeue failed") + } + if requeueDuration >= 0 { + if requeueDuration == 0 { + requeueDuration = DefaultRequeueDuration + } + return ctrl.Result{RequeueAfter: requeueDuration}, nil + } else { + return ctrl.Result{}, nil + } +} + +// loadEngInst +func (e *OpEngine) loadEngInst(ctx context.Context, obj client.Object) (engine RuleEngine, inst types.RedisInstance, err error) { + switch o := obj.(type) { + case *v1alpha1.DistributedRedisCluster: + inst, err = clustermodel.NewRedisCluster(ctx, e.client, e.eventRecorder, o, e.logger) + engine = e.clusterRuleEngine + case *v1.RedisFailover: + inst, err = failovermodel.NewRedisFailover(ctx, e.client, e.eventRecorder, o, e.logger) + engine = e.failoverRuleEngine + case *v1.RedisSentinel: + inst, err = sentinelmodel.NewRedisSentinel(ctx, e.client, e.eventRecorder, o, e.logger) + engine = e.sentinelRuleEngine + default: + return nil, nil, fmt.Errorf("unknown type %T", obj) + } + return +} diff --git a/internal/ops/sentinel/actor/actor_ensure_resource.go b/internal/ops/sentinel/actor/actor_ensure_resource.go new file mode 100644 index 0000000..45ea6be --- /dev/null +++ b/internal/ops/sentinel/actor/actor_ensure_resource.go @@ -0,0 +1,640 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "fmt" + "reflect" + "slices" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + "github.com/alauda/redis-operator/internal/config" + ops "github.com/alauda/redis-operator/internal/ops/sentinel" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/go-logr/logr" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ actor.Actor = (*actorEnsureResource)(nil) + +func init() { + actor.Register(core.RedisStdSentinel, NewEnsureResourceActor) +} + +func NewEnsureResourceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorEnsureResource{ + client: client, + logger: logger, + } +} + +type actorEnsureResource struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorEnsureResource) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandEnsureResource} +} + +func (a *actorEnsureResource) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +// Do +func (a *actorEnsureResource) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandEnsureResource.String()) + + var ( + sentinel = val.(types.RedisSentinelInstance) + inst = sentinel.Definition() + ) + if inst.Spec.PodAnnotations[config.PAUSE_ANNOTATION_KEY] != "" { + if ret := a.ensurePauseStatefulSet(ctx, sentinel, logger); ret != nil { + return ret + } + return actor.NewResult(ops.CommandPaused) + } + + if ret := a.ensureServiceAccount(ctx, sentinel, logger); ret != nil { + return ret + } + if ret := a.ensureService(ctx, sentinel, logger); ret != nil { + return ret + } + // ensure configMap + if ret := a.ensureConfigMap(ctx, sentinel, logger); ret != nil { + return ret + } + if ret := a.ensureRedisSSL(ctx, sentinel, logger); ret != nil { + return ret + } + if ret := a.ensureStatefulSet(ctx, sentinel, logger); ret != nil { + return ret + } + return nil +} + +func (a *actorEnsureResource) ensureStatefulSet(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + if ret := a.ensurePodDisruptionBudget(ctx, inst, logger); ret != nil { + return ret + } + + sen := inst.Definition() + selector := inst.Selector() + + salt := fmt.Sprintf("%s-%s-%s", sen.GetName(), sen.GetNamespace(), sen.GetName()) + sts := sentinelbuilder.NewSentinelStatefulset(inst, selector) + if sts.Spec.Template.Annotations == nil { + sts.Spec.Template.Annotations = make(map[string]string) + } + if sen.Spec.PasswordSecret != "" { + secret, err := a.client.GetSecret(ctx, sen.Namespace, sen.Spec.PasswordSecret) + if err != nil { + logger.Error(err, "get password secret failed", "target", util.ObjectKey(sen.Namespace, sen.Spec.PasswordSecret)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + secretSig, err := util.GenerateObjectSig(secret, salt) + if err != nil { + logger.Error(err, "generate secret sig failed") + return actor.NewResultWithError(ops.CommandAbort, err) + } + sts.Spec.Template.Annotations[builder.PasswordSigAnnotationKey] = secretSig + } + + configName := sentinelbuilder.GetSentinelConfigMapName(sen.Name) + configMap, err := a.client.GetConfigMap(ctx, sen.Namespace, configName) + if err != nil { + logger.Error(err, "get configMap failed", "target", util.ObjectKey(sen.Namespace, configName)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + configSig, err := util.GenerateObjectSig(configMap, salt) + if err != nil { + logger.Error(err, "generate configMap sig failed") + return actor.NewResultWithError(ops.CommandAbort, err) + } + sts.Spec.Template.Annotations[builder.ConfigSigAnnotationKey] = configSig + + oldSts, err := a.client.GetStatefulSet(ctx, sts.Namespace, sts.Name) + if errors.IsNotFound(err) { + if err := a.client.CreateStatefulSet(ctx, sen.Namespace, sts); err != nil { + logger.Error(err, "create statefulset failed", "target", client.ObjectKeyFromObject(sts)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else if err != nil { + logger.Error(err, "get statefulset failed", "target", client.ObjectKeyFromObject(sts)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } else if clusterbuilder.IsStatefulsetChanged(sts, oldSts, logger) { + if *oldSts.Spec.Replicas > *sts.Spec.Replicas { + oldSts.Spec.Replicas = sts.Spec.Replicas + if err := a.client.UpdateStatefulSet(ctx, sen.Namespace, oldSts); err != nil { + logger.Error(err, "scale down statefulset failed", "target", client.ObjectKeyFromObject(oldSts)) + return actor.RequeueWithError(err) + } + time.Sleep(time.Second * 3) + } + + pods, err := inst.RawNodes(ctx) + if err != nil { + logger.Error(err, "get pods failed") + return actor.RequeueWithError(err) + } + for _, item := range pods { + pod := item.DeepCopy() + pod.Labels = lo.Assign(pod.Labels, inst.Selector()) + if !reflect.DeepEqual(pod.Labels, item.Labels) { + if err := a.client.UpdatePod(ctx, pod.GetNamespace(), pod); err != nil { + logger.Error(err, "patch pod label failed", "target", client.ObjectKeyFromObject(pod)) + return actor.RequeueWithError(err) + } + } + } + + if err := a.client.DeleteStatefulSet(ctx, sen.Namespace, sts.GetName(), + client.PropagationPolicy(metav1.DeletePropagationOrphan)); err != nil && !errors.IsNotFound(err) { + + logger.Error(err, "delete old statefulset failed", "target", client.ObjectKeyFromObject(sts)) + return actor.RequeueWithError(err) + } + time.Sleep(time.Second * 3) + if err = a.client.CreateStatefulSet(ctx, sen.Namespace, sts); err != nil { + logger.Error(err, "update statefulset failed", "target", client.ObjectKeyFromObject(sts)) + return actor.RequeueWithError(err) + } + } + return nil +} + +func (a *actorEnsureResource) ensurePodDisruptionBudget(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + sen := inst.Definition() + + pdb := sentinelbuilder.NewPodDisruptionBudget(sen, inst.Selector()) + if oldPdb, err := a.client.GetPodDisruptionBudget(ctx, sen.Namespace, pdb.Name); errors.IsNotFound(err) { + if err := a.client.CreatePodDisruptionBudget(ctx, sen.Namespace, pdb); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else if err != nil { + logger.Error(err, "get poddisruptionbudget failed", "target", client.ObjectKeyFromObject(pdb)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } else if !reflect.DeepEqual(oldPdb.Spec, pdb.Spec) { + pdb.ResourceVersion = oldPdb.ResourceVersion + if err := a.client.UpdatePodDisruptionBudget(ctx, sen.Namespace, pdb); err != nil { + logger.Error(err, "update poddisruptionbudget failed", "target", client.ObjectKeyFromObject(pdb)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + return nil +} + +func (a *actorEnsureResource) ensureConfigMap(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + // ensure sentinel config + senitnelConfigMap, _ := sentinelbuilder.NewSentinelConfigMap(inst.Definition(), inst.Selector()) + if _, err := a.client.GetConfigMap(ctx, inst.GetNamespace(), senitnelConfigMap.Name); errors.IsNotFound(err) { + if err := a.client.CreateIfNotExistsConfigMap(ctx, inst.GetNamespace(), senitnelConfigMap); err != nil { + logger.Error(err, "create configMap failed", "target", client.ObjectKeyFromObject(senitnelConfigMap)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else if err != nil { + logger.Error(err, "get configMap failed", "target", client.ObjectKeyFromObject(senitnelConfigMap)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + if err := a.client.UpdateIfConfigMapChanged(ctx, senitnelConfigMap); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + return nil +} + +func (a *actorEnsureResource) ensureRedisSSL(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + def := inst.Definition() + if !def.Spec.EnableTLS { + return nil + } + + if secretName := def.Spec.ExternalTLSSecret; secretName != "" { + var ( + err error + secret *corev1.Secret + ) + for i := 0; i < 5; i++ { + if secret, err = a.client.GetSecret(ctx, def.Namespace, secretName); err != nil { + logger.Error(err, "get external tls secret failed", "name", secretName) + time.Sleep(time.Second * time.Duration(i+1)) + } else { + break + } + } + if secret == nil { + logger.Error(err, "get external tls secret failed", "name", secretName) + return actor.RequeueWithError(fmt.Errorf("external tls secret %s not found", secretName)) + } + return nil + } + + cert := sentinelbuilder.NewCertificate(def, inst.Selector()) + if err := a.client.CreateIfNotExistsCertificate(ctx, def.Namespace, cert); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + oldCert, err := a.client.GetCertificate(ctx, def.Namespace, cert.GetName()) + if err != nil && !errors.IsNotFound(err) { + logger.Error(err, "get certificate failed", "target", client.ObjectKeyFromObject(cert)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + + var ( + secretName = builder.GetRedisSSLSecretName(def.Name) + secret *corev1.Secret + ) + for i := 0; i < 5; i++ { + if secret, _ = a.client.GetSecret(ctx, def.Namespace, secretName); secret != nil { + break + } + // check when the certificate created + if time.Since(oldCert.GetCreationTimestamp().Time) > time.Minute*5 { + return actor.NewResultWithError(ops.CommandAbort, fmt.Errorf("issue for tls certificate failed, please check the cert-manager")) + } + time.Sleep(time.Second * time.Duration(i+1)) + } + if secret == nil { + return actor.NewResult(ops.CommandRequeue) + } + return nil +} + +func (a *actorEnsureResource) ensurePauseStatefulSet(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + sen := inst.Definition() + name := sentinelbuilder.GetSentinelStatefulSetName(sen.Name) + if sts, err := a.client.GetStatefulSet(ctx, sen.Namespace, name); err != nil { + if errors.IsNotFound(err) { + return nil + } + return actor.NewResultWithError(ops.CommandRequeue, err) + } else { + if sts.Spec.Replicas == nil || *sts.Spec.Replicas == 0 { + return nil + } + *sts.Spec.Replicas = 0 + if err = a.client.UpdateStatefulSet(ctx, sen.Namespace, sts); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + return nil +} + +func (a *actorEnsureResource) ensureServiceAccount(ctx context.Context, sentinel types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + sen := sentinel.Definition() + sa := clusterbuilder.NewServiceAccount(sen) + role := clusterbuilder.NewRole(sen) + binding := clusterbuilder.NewRoleBinding(sen) + clusterRole := clusterbuilder.NewClusterRole(sen) + clusterRoleBinding := clusterbuilder.NewClusterRoleBinding(sen) + + if err := a.client.CreateOrUpdateServiceAccount(ctx, sentinel.GetNamespace(), sa); err != nil { + logger.Error(err, "create service account failed", "target", client.ObjectKeyFromObject(sa)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + if err := a.client.CreateOrUpdateRole(ctx, sentinel.GetNamespace(), role); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + if err := a.client.CreateOrUpdateRoleBinding(ctx, sentinel.GetNamespace(), binding); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + if err := a.client.CreateOrUpdateClusterRole(ctx, clusterRole); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + if oldClusterRb, err := a.client.GetClusterRoleBinding(ctx, clusterRoleBinding.Name); err != nil { + if errors.IsNotFound(err) { + if err := a.client.CreateClusterRoleBinding(ctx, clusterRoleBinding); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else { + exists := false + for _, sub := range oldClusterRb.Subjects { + if sub.Namespace == sentinel.GetNamespace() { + exists = true + } + } + if !exists && len(oldClusterRb.Subjects) > 0 { + oldClusterRb.Subjects = append(oldClusterRb.Subjects, + rbacv1.Subject{Kind: "ServiceAccount", + Name: clusterbuilder.RedisInstanceServiceAccountName, + Namespace: sentinel.GetNamespace()}, + ) + err := a.client.CreateOrUpdateClusterRoleBinding(ctx, oldClusterRb) + if err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + } + return nil +} + +func (a *actorEnsureResource) ensureService(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + sen := inst.Definition() + selector := inst.Selector() + + if ret := a.cleanUselessService(ctx, inst, logger); ret != nil { + return ret + } + + createService := func(senService *corev1.Service) *actor.ActorResult { + if oldService, err := a.client.GetService(ctx, sen.GetNamespace(), senService.Name); errors.IsNotFound(err) { + if err := a.client.CreateService(ctx, sen.GetNamespace(), senService); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else if err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } else if senService.Spec.Type != oldService.Spec.Type || + (senService.Spec.Type == corev1.ServiceTypeNodePort && senService.Spec.Ports[0].NodePort != oldService.Spec.Ports[0].NodePort) || + !reflect.DeepEqual(senService.Spec.Selector, oldService.Spec.Selector) || + !reflect.DeepEqual(senService.Labels, oldService.Labels) || + !reflect.DeepEqual(senService.Annotations, oldService.Annotations) { + + if err := a.client.UpdateService(ctx, sen.GetNamespace(), senService); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + return nil + } + + if ret := createService(sentinelbuilder.NewSentinelHeadlessServiceForCR(sen, selector)); ret != nil { + return ret + } + + switch sen.Spec.Expose.ServiceType { + case corev1.ServiceTypeNodePort: + if ret := a.ensureRedisSpecifiedNodePortService(ctx, inst, logger); ret != nil { + return ret + } + case corev1.ServiceTypeLoadBalancer: + if ret := a.ensureRedisPodService(ctx, inst, logger); ret != nil { + return ret + } + } + + if ret := createService(sentinelbuilder.NewSentinelServiceForCR(sen, selector)); ret != nil { + return ret + } + return nil +} + +func (a *actorEnsureResource) ensureRedisSpecifiedNodePortService(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + cr := inst.Definition() + + if cr.Spec.Expose.NodePortSequence == "" { + return a.ensureRedisPodService(ctx, inst, logger) + } + + logger.V(3).Info("ensure sentinel nodeports", "namepspace", cr.Namespace, "name", cr.Name) + configedPorts, err := helper.ParseSequencePorts(cr.Spec.Expose.NodePortSequence) + if err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + getClientPort := func(svc *corev1.Service, args ...string) int32 { + name := "sentinel" + if len(args) > 0 { + name = args[0] + } + if port := util.GetServicePortByName(svc, name); port != nil { + return port.NodePort + } + return 0 + } + + serviceNameRange := map[string]struct{}{} + for i := 0; i < int(cr.Spec.Replicas); i++ { + serviceName := sentinelbuilder.GetSentinelNodeServiceName(cr.GetName(), i) + serviceNameRange[serviceName] = struct{}{} + } + senNodePortSvc, err := a.client.GetService(ctx, cr.GetNamespace(), sentinelbuilder.GetSentinelServiceName(cr.GetName())) + if err != nil && !errors.IsNotFound(err) { + logger.Error(err, "get nodeport service failed", "target", sentinelbuilder.GetSentinelServiceName(cr.GetName())) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + + // the whole process is divided into 3 steps: + // 1. delete service not in nodeport range + // 2. create new service + // 3. update existing service and restart pod (only one pod is restarted at a same time for each shard) + + // 1. delete service not in nodeport range + // + // when pod not exists and service not in nodeport range, delete service + // NOTE: only delete service whose pod is not found + // let statefulset auto scale up/down for pods + selector := sentinelbuilder.GenerateSelectorLabels("sentinel", cr.Name) + services, ret := a.fetchAllPodBindedServices(ctx, cr.Namespace, selector) + if ret != nil { + return ret + } + for _, svc := range services { + svc := svc.DeepCopy() + occupiedPort := getClientPort(svc) + if _, exists := serviceNameRange[svc.Name]; !exists || !slices.Contains(configedPorts, occupiedPort) { + _, err := a.client.GetPod(ctx, svc.Namespace, svc.Name) + if errors.IsNotFound(err) { + logger.Info("release nodeport service", "service", svc.Name, "port", occupiedPort) + if err = a.client.DeleteService(ctx, svc.Namespace, svc.Name); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else if err != nil { + logger.Error(err, "get pods failed", "target", client.ObjectKeyFromObject(svc)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + } + if senNodePortSvc != nil && slices.Contains(configedPorts, senNodePortSvc.Spec.Ports[0].NodePort) { + // delete cluster nodeport service + if err := a.client.DeleteService(ctx, cr.GetNamespace(), senNodePortSvc.Name); err != nil { + a.logger.Error(err, "delete service failed", "target", client.ObjectKeyFromObject(senNodePortSvc)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + + if services, ret = a.fetchAllPodBindedServices(ctx, cr.Namespace, selector); ret != nil { + return ret + } + + // 2. create new service + var ( + newPorts []int32 + bindedNodeports []int32 + needUpdateServices []*corev1.Service + ) + for _, svc := range services { + bindedNodeports = append(bindedNodeports, getClientPort(svc.DeepCopy())) + } + + // filter used ports + for _, port := range configedPorts { + if !slices.Contains(bindedNodeports, port) { + newPorts = append(newPorts, port) + } + } + for i := 0; i < int(cr.Spec.Replicas); i++ { + serviceName := sentinelbuilder.GetSentinelNodeServiceName(cr.Name, i) + oldService, err := a.client.GetService(ctx, cr.Namespace, serviceName) + if errors.IsNotFound(err) { + if len(newPorts) == 0 { + continue + } + port := newPorts[0] + svc := sentinelbuilder.NewPodNodePortService(cr, i, selector, port) + if err = a.client.CreateService(ctx, svc.Namespace, svc); err != nil { + a.logger.Error(err, "create nodeport service failed", "target", client.ObjectKeyFromObject(svc)) + return actor.NewResultWithValue(ops.CommandRequeue, err) + } + newPorts = newPorts[1:] + continue + } else if err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + + svc := sentinelbuilder.NewPodNodePortService(cr, i, selector, getClientPort(oldService)) + // check old service for compability + if !reflect.DeepEqual(oldService.Spec.Selector, svc.Spec.Selector) || + len(oldService.Spec.Ports) != len(svc.Spec.Ports) || + !reflect.DeepEqual(oldService.Labels, svc.Labels) || + !reflect.DeepEqual(oldService.Annotations, svc.Annotations) { + + oldService.OwnerReferences = util.BuildOwnerReferences(cr) + oldService.Spec = svc.Spec + oldService.Labels = svc.Labels + oldService.Annotations = svc.Annotations + if err := a.client.UpdateService(ctx, oldService.Namespace, oldService); err != nil { + a.logger.Error(err, "update nodeport service failed", "target", client.ObjectKeyFromObject(oldService)) + return actor.NewResultWithValue(ops.CommandRequeue, err) + } + } + if port := getClientPort(oldService); port != 0 && !slices.Contains(configedPorts, port) { + needUpdateServices = append(needUpdateServices, oldService) + } + } + + // 3. update existing service and restart pod (only one pod is restarted at a same time for each shard) + if len(needUpdateServices) > 0 && len(newPorts) > 0 { + port, svc := newPorts[0], needUpdateServices[0] + if sp := util.GetServicePortByName(svc, "sentinel"); sp != nil { + sp.NodePort = port + } + + // NOTE: here not make sure the failover success, because the nodeport updated, the communication will be failed + // in k8s, the nodeport can still access for sometime after the nodeport updated + // + // update service + if err = a.client.UpdateService(ctx, svc.Namespace, svc); err != nil { + a.logger.Error(err, "update nodeport service failed", "target", client.ObjectKeyFromObject(svc), "port", port) + return actor.NewResultWithValue(ops.CommandRequeue, err) + } + if pod, _ := a.client.GetPod(ctx, cr.Namespace, svc.Spec.Selector[builder.PodNameLabelKey]); pod != nil { + if err := a.client.DeletePod(ctx, cr.Namespace, pod.Name); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + return actor.NewResult(ops.CommandRequeue) + } + } + return nil +} + +func (a *actorEnsureResource) ensureRedisPodService(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + sen := inst.Definition() + for replica := 0; replica < int(sen.Spec.Replicas); replica++ { + newSvc := sentinelbuilder.NewPodService(sen, replica, inst.Selector()) + if svc, err := a.client.GetService(ctx, sen.Namespace, newSvc.Name); errors.IsNotFound(err) { + if err = a.client.CreateService(ctx, sen.Namespace, newSvc); err != nil { + logger.Error(err, "create service failed", "target", client.ObjectKeyFromObject(newSvc)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else if err != nil { + logger.Error(err, "get service failed", "target", client.ObjectKeyFromObject(newSvc)) + return actor.NewResult(ops.CommandRequeue) + } else if newSvc.Spec.Type != svc.Spec.Type || + !reflect.DeepEqual(newSvc.Spec.Selector, svc.Spec.Selector) || + !reflect.DeepEqual(newSvc.Labels, svc.Labels) || + !reflect.DeepEqual(newSvc.Annotations, svc.Annotations) { + svc.Spec = newSvc.Spec + if err = a.client.UpdateService(ctx, sen.Namespace, svc); err != nil { + logger.Error(err, "update service failed", "target", client.ObjectKeyFromObject(svc)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + } + return nil +} + +func (a *actorEnsureResource) cleanUselessService(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + sen := inst.Definition() + services, err := a.fetchAllPodBindedServices(ctx, sen.Namespace, inst.Selector()) + if err != nil { + return err + } + for _, item := range services { + svc := item.DeepCopy() + index, err := util.ParsePodIndex(svc.Name) + if err != nil { + logger.Error(err, "parse svc name failed", "target", client.ObjectKeyFromObject(svc)) + continue + } + if index >= int(sen.Spec.Replicas) { + _, err := a.client.GetPod(ctx, svc.Namespace, svc.Name) + if errors.IsNotFound(err) { + if err = a.client.DeleteService(ctx, svc.Namespace, svc.Name); err != nil { + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } else if err != nil { + logger.Error(err, "get pods failed", "target", client.ObjectKeyFromObject(svc)) + return actor.NewResultWithError(ops.CommandRequeue, err) + } + } + } + return nil +} + +func (a *actorEnsureResource) fetchAllPodBindedServices(ctx context.Context, namespace string, labels map[string]string) ([]corev1.Service, *actor.ActorResult) { + var ( + services []corev1.Service + ) + + if svcRes, err := a.client.GetServiceByLabels(ctx, namespace, labels); err != nil { + return nil, actor.NewResultWithError(ops.CommandRequeue, err) + } else { + // ignore services without pod selector + for _, svc := range svcRes.Items { + if svc.Spec.Selector[builder.PodNameLabelKey] != "" { + services = append(services, svc) + } + } + } + return services, nil +} diff --git a/internal/ops/sentinel/actor/actor_heal_monitor.go b/internal/ops/sentinel/actor/actor_heal_monitor.go new file mode 100644 index 0000000..b17e42f --- /dev/null +++ b/internal/ops/sentinel/actor/actor_heal_monitor.go @@ -0,0 +1,103 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "slices" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + ops "github.com/alauda/redis-operator/internal/ops/sentinel" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" +) + +var _ actor.Actor = (*actorHealMonitor)(nil) + +func init() { + actor.Register(core.RedisStdSentinel, NewHealMonitorActor) +} + +func NewHealMonitorActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorHealMonitor{ + client: client, + logger: logger, + } +} + +type actorHealMonitor struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorHealMonitor) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandHealMonitor} +} + +func (a *actorHealMonitor) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +func (a *actorHealMonitor) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandHealMonitor.String()) + inst := val.(types.RedisSentinelInstance) + + // NOTE: only try to heal sentinel monitoring clusters when all nodes of sentinel is ready + if !inst.Replication().IsReady() { + logger.Info("resource is not ready") + return actor.NewResult(ops.CommandHealPod) + } + + var ( + clusters []string + ) + for _, node := range inst.Nodes() { + if vals, err := node.MonitoringClusters(ctx); err != nil { + logger.Error(err, "failed to get monitoring clusters") + } else { + for _, v := range vals { + if !slices.Contains(clusters, v) { + clusters = append(clusters, v) + } + } + } + } + // list all sentinels + for _, name := range clusters { + reseted := false + for _, node := range inst.Nodes() { + needReset := ops.NeedResetRedisSentinel(ctx, name, node, logger) + if needReset { + args := []any{"SENTINEL", "RESET", name} + if err := node.Setup(ctx, args); err != nil { + logger.Error(err, "failed to reset sentinel", "name", name) + } + reseted = true + } + } + if reseted { + inst.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, + "force reset sentinels %s", name) + } + } + return nil +} diff --git a/internal/ops/sentinel/actor/actor_heal_pod.go b/internal/ops/sentinel/actor/actor_heal_pod.go new file mode 100644 index 0000000..1ef9df0 --- /dev/null +++ b/internal/ops/sentinel/actor/actor_heal_pod.go @@ -0,0 +1,169 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + ops "github.com/alauda/redis-operator/internal/ops/sentinel" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + rtypes "github.com/alauda/redis-operator/pkg/types/redis" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ actor.Actor = (*actorHealPod)(nil) + +func init() { + actor.Register(core.RedisStdSentinel, NewHealPodActor) +} + +func NewHealPodActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { + return &actorHealPod{ + client: client, + logger: logger, + } +} + +type actorHealPod struct { + client kubernetes.ClientSet + logger logr.Logger +} + +func (a *actorHealPod) SupportedCommands() []actor.Command { + return []actor.Command{ops.CommandHealPod} +} + +func (a *actorHealPod) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +// Do +func (a *actorHealPod) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger().WithValues("actor", ops.CommandHealPod.String()) + + // clean terminating pods + var ( + inst = val.(types.RedisSentinelInstance) + now = time.Now() + isUpdated = false + ) + pods, err := inst.RawNodes(ctx) + if err != nil { + logger.Error(err, "failed to get pods") + return actor.RequeueWithError(err) + } + for _, pod := range pods { + timestamp := pod.GetDeletionTimestamp() + if timestamp == nil { + continue + } + + var node rtypes.RedisSentinelNode + slices.IndexFunc(inst.Nodes(), func(n rtypes.RedisSentinelNode) bool { + if n.GetName() == pod.GetName() { + node = n + return true + } + return false + }) + + grace := time.Second * 10 + if val := pod.GetDeletionGracePeriodSeconds(); val != nil && node != nil { + grace = time.Duration(*val) * time.Second + } + if now.Sub(timestamp.Time) <= grace { + continue + } + + objKey := client.ObjectKeyFromObject(pod.DeepCopy()) + logger.V(2).Info("for delete pod", "name", pod.GetName()) + // force delete the terminating pods + if err := a.client.DeletePod(ctx, inst.GetNamespace(), pod.GetName(), client.GracePeriodSeconds(0)); err != nil { + logger.Error(err, "force delete pod failed", "target", objKey) + } else { + inst.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, "force delete blocked terminating pod %s", objKey.Name) + logger.Info("force delete blocked terminating pod", "target", objKey) + isUpdated = true + } + } + if isUpdated { + return actor.NewResult(ops.CommandRequeue) + } + + if typ := inst.Definition().Spec.Expose.ServiceType; typ == corev1.ServiceTypeNodePort || + typ == corev1.ServiceTypeLoadBalancer { + for _, node := range inst.Nodes() { + if !node.IsReady() { + continue + } + announceIP := node.DefaultIP().String() + announcePort := node.Port() + svc, err := a.client.GetService(ctx, node.GetNamespace(), node.GetName()) + if errors.IsNotFound(err) { + logger.Info("service not found", "name", node.GetName()) + return actor.NewResult(ops.CommandEnsureResource) + } else if err != nil { + logger.Error(err, "get service failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } + if typ == corev1.ServiceTypeNodePort { + port := util.GetServicePortByName(svc, "sentinel") + if port != nil { + if int(port.NodePort) != announcePort { + if err := a.client.DeletePod(ctx, inst.GetNamespace(), node.GetName()); err != nil { + logger.Error(err, "delete pod failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } else { + inst.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, + "force delete pod with inconsist annotation %s", node.GetName()) + return actor.Requeue() + } + } + } else { + logger.Error(fmt.Errorf("service port not found"), "service port not found", "name", node.GetName(), "port", "sentinel") + } + } else if typ == corev1.ServiceTypeLoadBalancer { + if index := slices.IndexFunc(svc.Status.LoadBalancer.Ingress, func(ing corev1.LoadBalancerIngress) bool { + return ing.IP == announceIP || ing.Hostname == announceIP + }); index < 0 { + if err := a.client.DeletePod(ctx, inst.GetNamespace(), node.GetName()); err != nil { + logger.Error(err, "delete pod failed", "name", node.GetName()) + return actor.RequeueWithError(err) + } else { + inst.SendEventf(corev1.EventTypeWarning, config.EventCleanResource, + "force delete pod with inconsist annotation %s", node.GetName()) + return actor.Requeue() + } + } + } + } + } + return nil +} diff --git a/internal/ops/sentinel/command.go b/internal/ops/sentinel/command.go new file mode 100644 index 0000000..47efd5b --- /dev/null +++ b/internal/ops/sentinel/command.go @@ -0,0 +1,32 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinel + +import ( + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/pkg/actor" +) + +var ( + CommandRequeue = actor.CommandRequeue + CommandAbort = actor.CommandAbort + CommandPaused = actor.CommandPaused + + CommandEnsureResource actor.Command = actor.NewCommand(core.RedisStdSentinel, "CommandEnsureResource") + CommandHealPod actor.Command = actor.NewCommand(core.RedisStdSentinel, "CommandHealPod") + CommandHealMonitor actor.Command = actor.NewCommand(core.RedisStdSentinel, "CommandHealMonitor") +) diff --git a/internal/ops/sentinel/engine.go b/internal/ops/sentinel/engine.go new file mode 100644 index 0000000..2465951 --- /dev/null +++ b/internal/ops/sentinel/engine.go @@ -0,0 +1,201 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinel + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/actor" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/record" +) + +type RuleEngine struct { + client kubernetes.ClientSet + eventRecorder record.EventRecorder + logger logr.Logger +} + +func NewRuleEngine(client kubernetes.ClientSet, eventRecorder record.EventRecorder, logger logr.Logger) (*RuleEngine, error) { + if client == nil { + return nil, fmt.Errorf("require client set") + } + if eventRecorder == nil { + return nil, fmt.Errorf("require EventRecorder") + } + + ctrl := RuleEngine{ + client: client, + eventRecorder: eventRecorder, + logger: logger, + } + return &ctrl, nil +} + +func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *actor.ActorResult { + logger := val.Logger() + + sentinel := val.(types.RedisSentinelInstance) + if sentinel == nil { + return nil + } + logger.V(3).Info("Inspecting Sentinel") + + cr := sentinel.Definition() + if val := cr.Spec.PodAnnotations[config.PAUSE_ANNOTATION_KEY]; val != "" { + return actor.NewResult(CommandEnsureResource) + } + + // NOTE: checked if resource is fullfilled, expecially for pod binded services + if isFullfilled, _ := sentinel.IsResourceFullfilled(ctx); !isFullfilled { + return actor.NewResult(CommandEnsureResource) + } + + if ret := g.isPodHealNeeded(ctx, sentinel, logger); ret != nil { + return ret + } + + // check all registered replication and clean: + // 1. fail nodes + // 2. duplicate nodes with same id + // 3. unexcepted sentinel nodes ??? + if ret := g.isResetSentinelNeeded(ctx, sentinel, logger); ret != nil { + return ret + } + return actor.NewResult(CommandEnsureResource) +} + +func (g *RuleEngine) isPodHealNeeded(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + if pods, err := inst.RawNodes(ctx); err != nil { + logger.Error(err, "failed to get pods") + return actor.RequeueWithError(err) + } else if len(pods) > int(inst.Definition().Spec.Replicas) { + return actor.NewResult(CommandEnsureResource) + } + + // check if svc and pod in consistence + for i, node := range inst.Nodes() { + if i != node.Index() { + return actor.NewResult(CommandHealPod) + } + + if typ := inst.Definition().Spec.Expose.ServiceType; node.IsReady() && + (typ == corev1.ServiceTypeNodePort || typ == corev1.ServiceTypeLoadBalancer) { + announceIP := node.DefaultIP().String() + announcePort := node.Port() + svc, err := g.client.GetService(ctx, inst.GetNamespace(), node.GetName()) + if errors.IsNotFound(err) { + return actor.NewResult(CommandEnsureResource) + } else if err != nil { + return actor.RequeueWithError(err) + } + if typ == corev1.ServiceTypeNodePort { + port := util.GetServicePortByName(svc, "sentinel") + if port != nil { + if int(port.NodePort) != announcePort { + return actor.NewResult(CommandHealPod) + } + } else { + logger.Error(fmt.Errorf("service %s not found", node.GetName()), "failed to get service, which should not happen") + } + } else if typ == corev1.ServiceTypeLoadBalancer { + if slices.IndexFunc(svc.Status.LoadBalancer.Ingress, func(i corev1.LoadBalancerIngress) bool { + return i.IP == announceIP + }) < 0 { + return actor.NewResult(CommandHealPod) + } + } + } + } + return nil +} + +func (g *RuleEngine) isResetSentinelNeeded(ctx context.Context, inst types.RedisSentinelInstance, logger logr.Logger) *actor.ActorResult { + var clusters []string + for _, node := range inst.Nodes() { + if vals, err := node.MonitoringClusters(ctx); err != nil { + logger.Error(err, "failed to get monitoring clusters") + } else { + for _, v := range vals { + if !slices.Contains(clusters, v) { + clusters = append(clusters, v) + } + } + } + } + + for _, name := range clusters { + for _, node := range inst.Nodes() { + needReset := NeedResetRedisSentinel(ctx, name, node, logger) + if needReset { + return actor.NewResult(CommandHealMonitor) + } + } + } + return nil +} + +func NeedResetRedisSentinel(ctx context.Context, name string, node redis.RedisSentinelNode, logger logr.Logger) bool { + master, replicas, err := node.MonitoringNodes(ctx, name) + if err != nil { + logger.Error(err, "failed to get monitoring nodes", "name", name) + return false + } + if master == nil || strings.Contains(master.Flags, "o_down") { + return true + } + + ids := map[string]struct{}{master.RunId: {}} + for _, repl := range replicas { + if strings.Contains(repl.Flags, "o_down") { + return true + } + if _, ok := ids[repl.RunId]; ok { + return true + } + ids[repl.RunId] = struct{}{} + } + + brothers, err := node.Brothers(ctx, name) + if err != nil { + logger.Error(err, "failed to get brothers", "name", name) + return false + } + ids = map[string]struct{}{master.RunId: {}} + for _, b := range brothers { + // sentinel nodes will not set node to o_down, always keep in s_down status + if strings.Contains(b.Flags, "o_down") { + return true + } + if _, ok := ids[b.RunId]; ok { + return true + } + ids[b.RunId] = struct{}{} + } + return false +} diff --git a/pkg/models/cluster/cluster.go b/internal/redis/cluster/cluster.go similarity index 50% rename from pkg/models/cluster/cluster.go rename to internal/redis/cluster/cluster.go index aa28674..c16b69b 100644 --- a/pkg/models/cluster/cluster.go +++ b/internal/redis/cluster/cluster.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -21,54 +21,64 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "slices" + "strconv" "strings" - clusterv1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/redis/v1" + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/util" clientset "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" "github.com/alauda/redis-operator/pkg/security/acl" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/record" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" ) -var _ types.RedisInstance = (*RedisCluster)(nil) +var ( + _ types.RedisInstance = (*RedisCluster)(nil) + _ types.RedisClusterInstance = (*RedisCluster)(nil) +) type RedisCluster struct { clusterv1.DistributedRedisCluster - ctx context.Context - client clientset.ClientSet - redisUsers []*redismiddlewarealaudaiov1.RedisUser - shards []types.RedisClusterShard - // version redis.RedisVersion - users acl.Users - tlsConfig *tls.Config - configmap map[string]string + client clientset.ClientSet + eventRecorder record.EventRecorder + redisUsers []*redismiddlewarealaudaiov1.RedisUser + shards []types.RedisClusterShard + users acl.Users + tlsConfig *tls.Config logger logr.Logger } // NewRedisCluster -func NewRedisCluster(ctx context.Context, k8sClient clientset.ClientSet, def *clusterv1.DistributedRedisCluster, logger logr.Logger) (*RedisCluster, error) { +func NewRedisCluster(ctx context.Context, k8sClient clientset.ClientSet, eventRecorder record.EventRecorder, def *clusterv1.DistributedRedisCluster, logger logr.Logger) (*RedisCluster, error) { cluster := RedisCluster{ DistributedRedisCluster: *def, - ctx: ctx, - client: k8sClient, - configmap: map[string]string{}, - logger: logger.WithName("RedisCluster"), + + client: k8sClient, + eventRecorder: eventRecorder, + logger: logger.WithName("C").WithValues("instanec", client.ObjectKeyFromObject(def).String()), } var err error - // load after shard if cluster.users, err = cluster.loadUsers(ctx); err != nil { cluster.logger.Error(err, "loads users failed") @@ -82,7 +92,7 @@ func NewRedisCluster(ctx context.Context, k8sClient clientset.ClientSet, def *cl } // load shards - if cluster.shards, err = LoadRedisClusterShards(ctx, k8sClient, &cluster, logger); err != nil { + if cluster.shards, err = LoadRedisClusterShards(ctx, k8sClient, &cluster, cluster.logger); err != nil { cluster.logger.Error(err, "loads cluster shards failed", "cluster", def.Name) return nil, err } @@ -94,6 +104,14 @@ func NewRedisCluster(ctx context.Context, k8sClient clientset.ClientSet, def *cl return &cluster, nil } +func (c *RedisCluster) Arch() redis.RedisArch { + return core.RedisCluster +} + +func (c *RedisCluster) NamespacedName() client.ObjectKey { + return client.ObjectKey{Namespace: c.GetNamespace(), Name: c.GetName()} +} + func (c *RedisCluster) LoadRedisUsers(ctx context.Context) { oldOpUser, _ := c.client.GetRedisUser(ctx, c.GetNamespace(), clusterbuilder.GenerateClusterOperatorsRedisUserName(c.GetName())) oldDefultUser, _ := c.client.GetRedisUser(ctx, c.GetNamespace(), clusterbuilder.GenerateClusterDefaultRedisUserName(c.GetName())) @@ -101,7 +119,7 @@ func (c *RedisCluster) LoadRedisUsers(ctx context.Context) { } // ctx -func (c *RedisCluster) Restart(ctx context.Context) error { +func (c *RedisCluster) Restart(ctx context.Context, annotationKeyVal ...string) error { if c == nil { return nil } @@ -142,7 +160,7 @@ func (c *RedisCluster) Refresh(ctx context.Context) error { logger.Error(err, "get DistributedRedisCluster failed") return err } - _ = cr.Init() + _ = cr.Default() c.DistributedRedisCluster = cr var err error @@ -158,134 +176,181 @@ func (c *RedisCluster) Refresh(ctx context.Context) error { return nil } -// UpdateStatus -func (c *RedisCluster) UpdateStatus(ctx context.Context, status clusterv1.ClusterStatus, message string, shards []*clusterv1.ClusterShards) error { +// RewriteShards +func (c *RedisCluster) RewriteShards(ctx context.Context, shards []*clusterv1.ClusterShards) error { + if c == nil || len(shards) == 0 { + return nil + } + logger := c.logger.WithName("RewriteShards") + + if err := c.Refresh(ctx); err != nil { + return err + } + cr := &c.DistributedRedisCluster + if len(cr.Status.Shards) == 0 || c.IsInService() { + // only update shards when cluster in service + cr.Status.Shards = shards + } + if err := c.client.UpdateDistributedRedisClusterStatus(ctx, cr); err != nil { + logger.Error(err, "update DistributedRedisCluster status failed") + return err + } + return c.UpdateStatus(ctx, types.Any, "") +} + +func (c *RedisCluster) UpdateStatus(ctx context.Context, st types.InstanceStatus, message string) error { if c == nil { return nil } logger := c.logger.WithName("UpdateStatus") - logger.Info("UpdateStatus") + if err := c.Refresh(ctx); err != nil { + return err + } - cr := &c.DistributedRedisCluster + var ( + cr = &c.DistributedRedisCluster + status clusterv1.ClusterStatus + isResourceReady = (len(c.shards) == int(cr.Spec.MasterSize)) + isRollingRestart = false + isSlotMigrating = false + allSlots = slot.NewSlots() + unSchedulePods []string + messages []string + ) + switch st { + case types.OK: + status = clusterv1.ClusterStatusOK + case types.Fail: + status = clusterv1.ClusterStatusKO + case types.Paused: + status = clusterv1.ClusterStatusPaused + } + if message != "" { + messages = append(messages, message) + } cr.Status.ClusterStatus = clusterv1.ClusterOutOfService if c.IsInService() { cr.Status.ClusterStatus = clusterv1.ClusterInService } - // force update the message - if status != "" { - if err := c.client.Client().Get(ctx, client.ObjectKeyFromObject(cr), cr); err != nil { - logger.Error(err, "get DistributedRedisCluster failed") - if errors.IsNotFound(err) { - return nil - } - return err - } - cr.Status.Status = status - cr.Status.Reason = message - if len(cr.Status.Shards) == 0 || status == clusterv1.ClusterStatusRebalancing { - cr.Status.Shards = shards - } - } else { - if err := c.Refresh(ctx); err != nil { - return err - } - cr.Status.ClusterStatus = clusterv1.ClusterOutOfService - if c.IsInService() { - cr.Status.ClusterStatus = clusterv1.ClusterInService - } - - var ( - isResourceReady = (len(c.shards) == int(cr.Spec.MasterSize)) - isRollingRestart = false - isSlotMigrating = false - isHealthy = true - allSlots = slot.NewSlots() - unSchedulePods []string - ) - for _, shards := range cr.Status.Shards { - for _, status := range shards.Slots { - if status.Status == slot.SlotMigrating.String() || status.Status == slot.SlotImporting.String() { - isSlotMigrating = true - } +__end_slot_migrating__: + for _, shards := range cr.Status.Shards { + for _, status := range shards.Slots { + if status.Status == slot.SlotMigrating.String() || status.Status == slot.SlotImporting.String() { + isSlotMigrating = true + break __end_slot_migrating__ } } + } - // check if all resources fullfilled - for i, shard := range c.shards { - if i != shard.Index() || shard.Status().ReadyReplicas != cr.Spec.ClusterReplicas+1 { - isHealthy = false - isResourceReady = false - break - } - if len(shard.Replicas()) != int(cr.Spec.ClusterReplicas) { - isHealthy = false - } - - if shard.Status().UpdateRevision != "" && - (shard.Status().CurrentRevision != shard.Status().UpdateRevision || - *shard.Definition().Spec.Replicas != shard.Status().UpdatedReplicas) { + // check if all resources fullfilled + for i, shard := range c.shards { + if i != shard.Index() || + shard.Status().ReadyReplicas != cr.Spec.ClusterReplicas+1 || + len(shard.Replicas()) != int(cr.Spec.ClusterReplicas) { + isResourceReady = false + } - isRollingRestart = true - } else if shard.Definition().Spec.Replicas != &shard.Status().ReadyReplicas { - isResourceReady = false - } - slots := shard.Slots() + if shard.Status().CurrentRevision != shard.Status().UpdateRevision && + (*shard.Definition().Spec.Replicas != shard.Status().ReadyReplicas || + shard.Status().UpdatedReplicas != shard.Status().Replicas || + shard.Status().ReadyReplicas != shard.Status().CurrentReplicas) { + isRollingRestart = true + } + slots := shard.Slots() + if i < len(cr.Status.Shards) { allSlots = allSlots.Union(slots) + } - // output message for pending pods - for _, node := range shard.Nodes() { - if node.Status() == corev1.PodPending { - for _, cond := range node.Definition().Status.Conditions { - if cond.Type == corev1.PodScheduled && cond.Status == corev1.ConditionFalse { - unSchedulePods = append(unSchedulePods, node.GetName()) - } + // output message for pending pods + for _, node := range shard.Nodes() { + if node.Status() == corev1.PodPending { + for _, cond := range node.Definition().Status.Conditions { + if cond.Type == corev1.PodScheduled && cond.Status == corev1.ConditionFalse { + unSchedulePods = append(unSchedulePods, node.GetName()) } } - if node.IsMasterFailed() { - isHealthy = false - } } } + } + if len(unSchedulePods) > 0 { + messages = append(messages, fmt.Sprintf("pods %s unschedulable", strings.Join(unSchedulePods, ","))) + } - if cr.Status.ClusterStatus == clusterv1.ClusterOutOfService && allSlots.Count(slot.SlotAssigned) > 0 { - isHealthy = false - subSlots := slot.NewFullSlots().Sub(allSlots) - message = fmt.Sprintf("slots %s missing", subSlots.String()) - } + if cr.Status.ClusterStatus == clusterv1.ClusterOutOfService && allSlots.Count(slot.SlotAssigned) > 0 { + subSlots := slot.NewFullSlots().Sub(allSlots) + messages = append(messages, fmt.Sprintf("slots %s missing", subSlots.String())) + } + if status != "" { + cr.Status.Status = status + } else { if isRollingRestart { cr.Status.Status = clusterv1.ClusterStatusRollingUpdate } else if isSlotMigrating { cr.Status.Status = clusterv1.ClusterStatusRebalancing } else if isResourceReady { - cr.Status.Status = clusterv1.ClusterStatusOK if cr.Status.ClusterStatus != clusterv1.ClusterInService { cr.Status.Status = clusterv1.ClusterStatusKO + } else { + cr.Status.Status = clusterv1.ClusterStatusOK + cr.Status.Reason = "OK" } - } else if isHealthy && cr.Status.ClusterStatus == clusterv1.ClusterInService { - cr.Status.Status = clusterv1.ClusterStatusOK - message = "OK" } else { cr.Status.Status = clusterv1.ClusterStatusCreating } + } + if cr.Status.Status == clusterv1.ClusterStatusRebalancing { + var migratingSlots []string + for _, shards := range cr.Status.Shards { + for _, status := range shards.Slots { + if status.Status == slot.SlotMigrating.String() { + migratingSlots = append(migratingSlots, status.Slots) + } + } + } + if len(migratingSlots) > 0 { + message = fmt.Sprintf("slots %s migrating", strings.Join(migratingSlots, ",")) + messages = append(messages, message) + } + } + cr.Status.Reason = strings.Join(messages, "; ") - // only update shards when cluster in service - if len(shards) > 0 && (len(cr.Status.Shards) == 0 || cr.Status.ClusterStatus == clusterv1.ClusterInService) { - cr.Status.Shards = shards + if cr.Status.Status == clusterv1.ClusterStatusOK && + c.Spec.Expose.ServiceType == corev1.ServiceTypeNodePort && + c.Spec.Expose.NodePortSequence != "" { + + nodeports := map[int32]struct{}{} + for _, node := range c.Nodes() { + if port := node.Definition().Labels[builder.PodAnnouncePortLabelKey]; port != "" { + val, _ := strconv.ParseInt(port, 10, 32) + nodeports[int32(val)] = struct{}{} + } } - if len(unSchedulePods) > 0 { - message = fmt.Sprintf("pods %s Unschedulable.%s", strings.Join(unSchedulePods, ", "), message) + assignedPorts, _ := helper.ParseSequencePorts(cr.Spec.Expose.NodePortSequence) + // check nodeport applied + notAppliedPorts := []string{} + for _, port := range assignedPorts { + if _, ok := nodeports[port]; !ok { + notAppliedPorts = append(notAppliedPorts, fmt.Sprintf("%d", port)) + } + } + if len(notAppliedPorts) > 0 { + cr.Status.Status = clusterv1.ClusterStatusRollingUpdate + cr.Status.Reason = fmt.Sprintf("nodeport %s not applied", notAppliedPorts) } - cr.Status.Reason = message } cr.Status.Nodes = cr.Status.Nodes[0:0] cr.Status.NumberOfMaster = 0 cr.Status.NodesPlacement = clusterv1.NodesPlacementInfoOptimal - nodePlacement := map[string]struct{}{} + var ( + nodePlacement = map[string]struct{}{} + detailedNodes []core.RedisDetailedNode + ) + // update master count and node info for _, shard := range c.shards { master := shard.Master() @@ -298,44 +363,60 @@ func (c *RedisCluster) UpdateStatus(ctx context.Context, status clusterv1.Cluste } else { nodePlacement[node.NodeIP().String()] = struct{}{} } - cnode := clusterv1.RedisClusterNode{ - ID: node.ID(), - Role: node.Role(), - MasterRef: node.MasterID(), - IP: node.DefaultIP().String(), - Port: fmt.Sprintf("%d", node.Port()), - PodName: node.GetName(), - StatefulSet: shard.GetName(), - NodeName: node.NodeIP().String(), - Slots: []string{}, + cnode := core.RedisDetailedNode{ + RedisNode: core.RedisNode{ + ID: node.ID(), + Role: node.Role(), + MasterRef: node.MasterID(), + IP: node.DefaultIP().String(), + Port: fmt.Sprintf("%d", node.Port()), + PodName: node.GetName(), + StatefulSet: shard.GetName(), + NodeName: node.NodeIP().String(), + Slots: []string{}, + }, + Version: node.Info().RedisVersion, + UsedMemory: node.Info().UsedMemory, + UsedMemoryDataset: node.Info().UsedMemoryDataset, } if v := node.Slots().String(); v != "" { cnode.Slots = append(cnode.Slots, v) } - cr.Status.Nodes = append(cr.Status.Nodes, cnode) + cr.Status.Nodes = append(cr.Status.Nodes, cnode.RedisNode) + detailedNodes = append(detailedNodes, cnode) } } - if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - status := cr.Status - if err := c.client.Client().Get(ctx, client.ObjectKeyFromObject(cr), cr); err != nil { - logger.Error(err, "get DistributedRedisCluster failed") - return err + // update status configmap + detailedStatus := clusterv1.NewDistributedRedisClusterDetailedStatus(&cr.Status, detailedNodes) + detailedStatusCM, _ := clusterbuilder.NewRedisClusterDetailedStatusConfigMap(c, detailedStatus) + if oldDetailedStatusCM, err := c.client.GetConfigMap(ctx, c.GetNamespace(), detailedStatusCM.Name); errors.IsNotFound(err) { + if err := c.client.CreateConfigMap(ctx, c.GetNamespace(), detailedStatusCM); err != nil { + logger.Error(err, "update detailed status failed") + } + } else if err != nil { + logger.Error(err, "get detailed status configmap failed") + } else if clusterbuilder.ShouldUpdateDetailedStatusConfigMap(oldDetailedStatusCM, detailedStatus) { + if err := c.client.UpdateConfigMap(ctx, c.GetNamespace(), detailedStatusCM); err != nil { + logger.Error(err, "update detailed status failed") } - cr.Status = status - return c.client.Client().Status().Update(ctx, cr) - }); errors.IsNotFound(err) { + } + + if cr.Status.DetailedStatusRef == nil { + cr.Status.DetailedStatusRef = &corev1.ObjectReference{} + cr.Status.DetailedStatusRef.Kind = "ConfigMap" + cr.Status.DetailedStatusRef.Name = detailedStatusCM.Name + } + if err := c.client.UpdateDistributedRedisClusterStatus(ctx, cr); errors.IsNotFound(err) { return nil } else if err != nil { logger.Error(err, "get DistributedRedisCluster failed") return err } - _ = cr.Init() - return nil } -// UpdateStatus +// Status return the status of the cluster func (c *RedisCluster) Status() *clusterv1.DistributedRedisClusterStatus { if c == nil { return nil @@ -380,6 +461,21 @@ func (c *RedisCluster) Nodes() []redis.RedisNode { return ret } +func (c *RedisCluster) RawNodes(ctx context.Context) ([]corev1.Pod, error) { + if c == nil { + return nil, nil + } + + selector := clusterbuilder.GetClusterStatefulsetSelectorLabels(c.GetName(), -1) + // load pods by statefulset selector + ret, err := c.client.GetStatefulSetPodsByLabels(ctx, c.GetNamespace(), selector) + if err != nil { + c.logger.Error(err, "loads pods of sentinel statefulset failed") + return nil, err + } + return ret.Items, nil +} + func (c *RedisCluster) Masters() []redis.RedisNode { var ret []redis.RedisNode for _, shard := range c.shards { @@ -388,10 +484,6 @@ func (c *RedisCluster) Masters() []redis.RedisNode { return ret } -func (c *RedisCluster) UntrustedNodes() []redis.RedisNode { - return nil -} - // IsInService func (c *RedisCluster) IsInService() bool { if c == nil { @@ -411,8 +503,7 @@ func (c *RedisCluster) IsReady() bool { for _, shard := range c.shards { status := shard.Status() if !(status.ReadyReplicas == *shard.Definition().Spec.Replicas && - ((status.CurrentRevision == status.UpdateRevision && status.UpdatedReplicas == status.ReadyReplicas) || - status.UpdateRevision == "")) { + ((status.CurrentRevision == status.UpdateRevision && status.CurrentReplicas == status.ReadyReplicas) || status.UpdateRevision == "")) { return false } @@ -437,6 +528,13 @@ func (c *RedisCluster) Users() (us acl.Users) { return } +func (c *RedisCluster) TLSConfig() *tls.Config { + if c == nil { + return nil + } + return c.tlsConfig +} + // TLS func (c *RedisCluster) TLS() *tls.Config { if c == nil { @@ -451,7 +549,6 @@ func (c *RedisCluster) loadUsers(ctx context.Context) (acl.Users, error) { name = clusterbuilder.GenerateClusterACLConfigMapName(c.GetName()) users acl.Users ) - // NOTE: load acl config first. if acl config not exists, then this may be // an old instance(upgrade from old redis or operator version). // migrate old password account to acl @@ -474,7 +571,7 @@ func (c *RedisCluster) loadUsers(ctx context.Context) (acl.Users, error) { sts, err := c.client.GetStatefulSet(ctx, c.GetNamespace(), statefulsetName) if err != nil { if !errors.IsNotFound(err) { - c.logger.Error(err, "load statefulset failed", "target", fmt.Sprintf("%s/%s", c.GetNamespace(), c.GetName())) + c.logger.Error(err, "load statefulset failed", "target", util.ObjectKey(c.GetNamespace(), c.GetName())) } continue } @@ -523,10 +620,10 @@ func (c *RedisCluster) loadUsers(ctx context.Context) (acl.Users, error) { } else { users = append(users, u) } - u, _ := user.NewUser(user.DefaultUserName, user.RoleDeveloper, nil) + u, _ := user.NewUser(user.DefaultUserName, user.RoleDeveloper, nil, c.Version().IsACL2Supported()) users = append(users, u) } else { - if u, err := user.NewUser(username, role, secret); err != nil { + if u, err := user.NewUser(username, role, secret, c.Version().IsACL2Supported()); err != nil { c.logger.Error(err, "init users failed") return nil, err } else { @@ -540,6 +637,32 @@ func (c *RedisCluster) loadUsers(ctx context.Context) (acl.Users, error) { c.logger.Error(err, "load acl failed") return nil, err } + + var ( + defaultUser = users.GetDefaultUser() + rule *user.Rule + ) + if len(defaultUser.Rules) > 0 { + rule = defaultUser.Rules[0] + } else { + rule = &user.Rule{} + } + if c.Version().IsACL2Supported() { + rule.Channels = []string{"*"} + } + + renameVal := c.Definition().Spec.Config[clusterbuilder.RedisConfig_RenameCommand] + renames, _ := clusterbuilder.ParseRenameConfigs(renameVal) + if len(renames) > 0 { + rule.DisallowedCommands = []string{} + for key, val := range renames { + if key != val && !slices.Contains(rule.DisallowedCommands, key) { + rule.DisallowedCommands = append(rule.DisallowedCommands, key) + } + } + } + defaultUser.Rules = append(defaultUser.Rules[0:0], rule) + return users, nil } @@ -581,6 +704,106 @@ func (c *RedisCluster) IsACLUserExists() bool { return true } +func (c *RedisCluster) IsResourceFullfilled(ctx context.Context) (bool, error) { + var ( + serviceKey = corev1.SchemeGroupVersion.WithKind("Service") + stsKey = appsv1.SchemeGroupVersion.WithKind("StatefulSet") + ) + resources := map[schema.GroupVersionKind][]string{ + serviceKey: {c.GetName()}, // + } + for i := 0; i < int(c.Spec.MasterSize); i++ { + headlessSvcName := clusterbuilder.ClusterHeadlessSvcName(c.GetName(), i) + resources[serviceKey] = append(resources[serviceKey], headlessSvcName) // - + + stsName := clusterbuilder.ClusterStatefulSetName(c.GetName(), i) + resources[stsKey] = append(resources[stsKey], stsName) + } + if c.Spec.Expose.ServiceType == corev1.ServiceTypeLoadBalancer || c.Spec.Expose.ServiceType == corev1.ServiceTypeNodePort { + resources[serviceKey] = append(resources[serviceKey], clusterbuilder.RedisNodePortSvcName(c.GetName())) // drc--nodeport + for i := 0; i < int(c.Spec.MasterSize); i++ { + stsName := clusterbuilder.ClusterStatefulSetName(c.GetName(), i) + for j := 0; j < int(c.Spec.ClusterReplicas+1); j++ { + resources[serviceKey] = append(resources[serviceKey], fmt.Sprintf("%s-%d", stsName, j)) + } + } + } + + for gvk, names := range resources { + for _, name := range names { + var obj unstructured.Unstructured + obj.SetGroupVersionKind(gvk) + + err := c.client.Client().Get(ctx, client.ObjectKey{Namespace: c.GetNamespace(), Name: name}, &obj) + if errors.IsNotFound(err) { + c.logger.V(3).Info("resource not found", "target", util.ObjectKey(c.GetNamespace(), name)) + return false, nil + } else if err != nil { + c.logger.Error(err, "get resource failed", "target", util.ObjectKey(c.GetNamespace(), name)) + return false, err + } + } + } + + for i := 0; i < int(c.Spec.MasterSize); i++ { + stsName := clusterbuilder.ClusterStatefulSetName(c.GetName(), i) + sts, err := c.client.GetStatefulSet(ctx, c.GetNamespace(), stsName) + if err != nil { + if errors.IsNotFound(err) { + c.logger.V(3).Info("statefulset not found", "target", util.ObjectKey(c.GetNamespace(), stsName)) + return false, nil + } + c.logger.Error(err, "get statefulset failed", "target", util.ObjectKey(c.GetNamespace(), stsName)) + return false, err + } + if sts.Spec.Replicas == nil || *sts.Spec.Replicas != c.Spec.ClusterReplicas+1 { + return false, nil + } + redisToolsImage := config.GetRedisToolsImage(c) + if redisToolsImage == "" { + return false, fmt.Errorf("redis-tools image not found") + } + spec := sts.Spec.Template.Spec + for _, container := range append(append([]corev1.Container{}, spec.InitContainers...), spec.Containers...) { + if !strings.Contains(container.Image, "middleware/redis-tools") { + continue + } + if container.Image != redisToolsImage { + return false, nil + } + } + } + return true, nil +} + +func (c *RedisCluster) IsACLAppliedToAll() bool { + if c == nil || !c.Version().IsACLSupported() { + return false + } + for _, shard := range c.Shards() { + for _, node := range shard.Nodes() { + if !node.CurrentVersion().IsACLSupported() || !node.IsACLApplied() { + return false + } + } + } + return true +} + +func (c *RedisCluster) Logger() logr.Logger { + if c == nil { + return logr.Discard() + } + return c.logger +} + +func (c *RedisCluster) SendEventf(eventtype, reason, messageFmt string, args ...interface{}) { + if c == nil { + return + } + c.eventRecorder.Eventf(c.Definition(), eventtype, reason, messageFmt, args...) +} + // loadTLS func (c *RedisCluster) loadTLS(ctx context.Context) (*tls.Config, error) { if c == nil { @@ -596,7 +819,7 @@ func (c *RedisCluster) loadTLS(ctx context.Context) (*tls.Config, error) { statefulsetName := clusterbuilder.ClusterStatefulSetName(c.GetName(), i) if sts, err := c.client.GetStatefulSet(ctx, c.GetNamespace(), statefulsetName); err != nil { if !errors.IsNotFound(err) { - c.logger.Error(err, "load statefulset failed", "target", fmt.Sprintf("%s/%s", c.GetNamespace(), c.GetName())) + c.logger.Error(err, "load statefulset failed", "target", util.ObjectKey(c.GetNamespace(), c.GetName())) } continue } else { @@ -631,7 +854,7 @@ func (c *RedisCluster) loadTLS(ctx context.Context) (*tls.Config, error) { caCertPool.AppendCertsFromPEM(secret.Data["ca.crt"]) return &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, // #nosec RootCAs: caCertPool, Certificates: []tls.Certificate{cert}, }, nil diff --git a/pkg/models/cluster/shards.go b/internal/redis/cluster/shard.go similarity index 79% rename from pkg/models/cluster/shards.go rename to internal/redis/cluster/shard.go index af8f35b..a737fb1 100644 --- a/pkg/models/cluster/shards.go +++ b/internal/redis/cluster/shard.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -24,15 +24,17 @@ import ( "strconv" "time" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + model "github.com/alauda/redis-operator/internal/redis" + "github.com/alauda/redis-operator/internal/util" clientset "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/models" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" appv1 "k8s.io/api/apps/v1" + apitypes "k8s.io/apimachinery/pkg/types" k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -49,7 +51,8 @@ func LoadRedisClusterShards(ctx context.Context, client clientset.ClientSet, clu return nil, err } else { for _, sts := range resp.Items { - if shard, err := NewRedisClusterShard(ctx, client, cluster, &sts, logger); err != nil { + sts := sts.DeepCopy() + if shard, err := NewRedisClusterShard(ctx, client, cluster, sts, logger); err != nil { logger.Error(err, fmt.Sprintf("parse shard %s failed", sts.GetName())) } else { shards = append(shards, shard) @@ -83,7 +86,7 @@ func NewRedisClusterShard(ctx context.Context, client clientset.ClientSet, clust users := cluster.Users() var err error - if shard.nodes, err = models.LoadRedisNodes(ctx, client, sts, users.GetOpUser(), logger); err != nil { + if shard.nodes, err = model.LoadRedisNodes(ctx, client, sts, users.GetOpUser(), logger); err != nil { logger.Error(err, "load shard nodes failed", "shard", sts.GetName()) return nil, err } @@ -101,6 +104,16 @@ type RedisClusterShard struct { logger logr.Logger } +func (s *RedisClusterShard) NamespacedName() apitypes.NamespacedName { + if s.StatefulSet.Namespace == "" || s.StatefulSet.Name == "" { + return apitypes.NamespacedName{} + } + return apitypes.NamespacedName{ + Namespace: s.StatefulSet.Namespace, + Name: s.StatefulSet.Name, + } +} + // Version func (s *RedisClusterShard) Version() redis.RedisVersion { if s == nil { @@ -146,7 +159,7 @@ func (s *RedisClusterShard) Master() redis.RedisNode { var emptyMaster redis.RedisNode for _, node := range s.nodes { // if the node joined, and is master, then it's the master - if node.Role() == redis.RedisRoleMaster && node.IsJoined() { + if node.Role() == core.RedisRoleMaster && node.IsJoined() { if node.Slots().Count(slot.SlotAssigned) > 0 || node.Slots().Count(slot.SlotImporting) > 0 { return node } @@ -165,7 +178,7 @@ func (s *RedisClusterShard) Replicas() []redis.RedisNode { } var replicas []redis.RedisNode for _, node := range s.nodes { - if node.Role() == redis.RedisRoleSlave { + if node.Role() == core.RedisRoleReplica { replicas = append(replicas, node) } } @@ -178,7 +191,7 @@ func (s *RedisClusterShard) Slots() *slot.Slots { return nil } for _, node := range s.nodes { - if node.IsJoined() && node.Role() == redis.RedisRoleMaster && + if node.IsJoined() && node.Role() == core.RedisRoleMaster && (node.Slots().Count(slot.SlotAssigned) > 0 || node.Slots().Count(slot.SlotImporting) > 0) { return node.Slots() } @@ -186,6 +199,13 @@ func (s *RedisClusterShard) Slots() *slot.Slots { return nil } +func (s *RedisClusterShard) IsReady() bool { + if s == nil { + return false + } + return s.Status().ReadyReplicas == *s.Spec.Replicas && s.Status().UpdateRevision == s.Status().CurrentRevision +} + // IsImporting func (s *RedisClusterShard) IsImporting() bool { if s == nil { @@ -223,17 +243,22 @@ func (s *RedisClusterShard) IsMigrating() bool { } // Restart -func (s *RedisClusterShard) Restart(ctx context.Context) error { +func (s *RedisClusterShard) Restart(ctx context.Context, annotationKeyVal ...string) error { // update all shards logger := s.logger.WithName("Restart") + kv := map[string]string{ + "kubectl.kubernetes.io/restartedAt": time.Now().Format(time.RFC3339Nano), + } + for i := 0; i < len(annotationKeyVal)-1; i += 2 { + kv[annotationKeyVal[i]] = annotationKeyVal[i+1] + } + data, _ := json.Marshal(map[string]interface{}{ "spec": map[string]interface{}{ "template": map[string]interface{}{ "metadata": map[string]interface{}{ - "annotations": map[string]string{ - "kubectl.kubernetes.io/restartedAt": time.Now().Format(time.RFC3339Nano), - }, + "annotations": kv, }, }, }, @@ -252,7 +277,7 @@ func (s *RedisClusterShard) Refresh(ctx context.Context) error { logger := s.logger.WithName("Refresh") var err error - if s.nodes, err = models.LoadRedisNodes(ctx, s.client, &s.StatefulSet, s.cluster.Users().GetOpUser(), logger); err != nil { + if s.nodes, err = model.LoadRedisNodes(ctx, s.client, &s.StatefulSet, s.cluster.Users().GetOpUser(), logger); err != nil { logger.Error(err, "load shard nodes failed", "shard", s.GetName()) return err } diff --git a/internal/redis/failover/monitor/manual_monitor.go b/internal/redis/failover/monitor/manual_monitor.go new file mode 100644 index 0000000..359491a --- /dev/null +++ b/internal/redis/failover/monitor/manual_monitor.go @@ -0,0 +1,284 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package monitor + +import ( + "context" + "fmt" + "net" + "net/netip" + "slices" + "strconv" + "time" + + "github.com/alauda/redis-operator/api/core" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/util" + clientset "github.com/alauda/redis-operator/pkg/kubernetes" + rediscli "github.com/alauda/redis-operator/pkg/redis" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ types.FailoverMonitor = (*ManualMonitor)(nil) + +type ManualMonitor struct { + client clientset.ClientSet + failover types.RedisFailoverInstance + resourceName string + + logger logr.Logger +} + +func NewManualMonitor(ctx context.Context, client clientset.ClientSet, inst types.RedisFailoverInstance) (*ManualMonitor, error) { + if client == nil { + return nil, fmt.Errorf("require clientset") + } + if inst == nil { + return nil, fmt.Errorf("require instance") + } + + m := &ManualMonitor{ + client: client, + failover: inst, + resourceName: fmt.Sprintf("rf-ha-repl-%s", inst.GetName()), + logger: inst.Logger(), + } + return m, nil +} + +func (s *ManualMonitor) Policy() databasesv1.FailoverPolicy { + return databasesv1.ManualFailoverPolicy +} + +func convertNodeInfoToSentinelNodeInfo(node redis.RedisNode) *rediscli.SentinelMonitorNode { + var ( + flag = "slave" + masterLinkStatus = lo.If(node.IsMasterLinkUp(), "ok").Else("down") + ) + if node.Role() == core.RedisRoleMaster { + flag = "master" + masterLinkStatus = "" + } + mnode := rediscli.SentinelMonitorNode{ + Name: "mymaster", + RunId: node.Info().RunId, + IP: node.DefaultIP().String(), + Port: fmt.Sprintf("%d", node.Port()), + MasterHost: node.Info().MasterHost, + MasterPort: node.Info().MasterPort, + Flags: flag, + MasterLinkStatus: masterLinkStatus, + } + return &mnode +} + +func (s *ManualMonitor) Master(ctx context.Context) (*rediscli.SentinelMonitorNode, error) { + cm, err := s.client.GetConfigMap(ctx, s.failover.GetNamespace(), s.resourceName) + if errors.IsNotFound(err) { + return nil, ErrNoMaster + } else if err != nil { + return nil, err + } + if addr := cm.Data["master"]; addr == "" { + return nil, ErrNoMaster + } else if ipPort, err := netip.ParseAddrPort(addr); err != nil { + return nil, err + } else { + var mnode *rediscli.SentinelMonitorNode + for _, node := range s.failover.Nodes() { + if node.DefaultIP().String() == ipPort.Addr().String() && node.Port() == int(ipPort.Port()) { + mnode = convertNodeInfoToSentinelNodeInfo(node) + break + } + } + if mnode == nil { + mnode = &rediscli.SentinelMonitorNode{ + Name: "mymaster", + IP: ipPort.Addr().String(), + Port: fmt.Sprintf("%d", ipPort.Port()), + Flags: "down,master", + } + } + return mnode, nil + } +} + +func (s *ManualMonitor) Replicas(ctx context.Context) (ret []*rediscli.SentinelMonitorNode, err error) { + for _, node := range s.failover.Nodes() { + if node.Role() == core.RedisRoleReplica { + ret = append(ret, convertNodeInfoToSentinelNodeInfo(node)) + } + } + return ret, nil +} + +func (s *ManualMonitor) Inited(ctx context.Context) (bool, error) { + if cm, err := s.client.GetConfigMap(ctx, s.failover.GetNamespace(), s.resourceName); errors.IsNotFound(err) { + return false, nil + } else if err != nil { + return false, err + } else { + return cm.Data["master"] != "", nil + } +} + +func (s *ManualMonitor) AllNodeMonitored(ctx context.Context) (bool, error) { + registeredNodes := map[string]struct{}{} + if masterNode, _ := s.Master(ctx); IsMonitoringNodeOnline(masterNode) { + registeredNodes[masterNode.Address()] = struct{}{} + } + replicas, _ := s.Replicas(ctx) + for _, replica := range replicas { + if IsMonitoringNodeOnline(replica) { + registeredNodes[replica.Address()] = struct{}{} + } + } + for _, node := range s.failover.Nodes() { + addr := net.JoinHostPort(node.DefaultIP().String(), strconv.Itoa(node.Port())) + addr2 := net.JoinHostPort(node.DefaultInternalIP().String(), strconv.Itoa(node.InternalPort())) + _, ok := registeredNodes[addr] + _, ok2 := registeredNodes[addr2] + if !ok && !ok2 { + return false, nil + } + } + return true, nil +} + +func (s *ManualMonitor) UpdateConfig(ctx context.Context, params map[string]string) error { + return nil +} + +func (s *ManualMonitor) Failover(ctx context.Context) error { + currentMaster, err := s.Master(ctx) + if err != nil && err != ErrNoMaster { + return err + } + + // find one replica to promote + if currentMaster != nil { + for _, node := range s.failover.Nodes() { + if node.DefaultIP().String() == currentMaster.IP && + strconv.Itoa(node.Port()) == currentMaster.Port && + node.Role() == core.RedisRoleMaster { + // self is master, not failover + return nil + } + } + } + + // master is down, find a replica to promote + var ( + masterCandidate redis.RedisNode + masterNodes = s.failover.Masters() + ) + if len(masterNodes) == 1 { + masterCandidate = masterNodes[0] + } else if nodes := s.failover.Nodes(); len(nodes) > 0 { + slices.SortStableFunc(nodes, func(i, j redis.RedisNode) int { + if i.Info().ConnectedReplicas > j.Info().ConnectedReplicas { + return -1 + } + return 1 + }) + if nodes[0].Info().ConnectedReplicas > 0 { + masterCandidate = nodes[0] + } + if masterCandidate == nil { + replIds := map[string]struct{}{} + for _, node := range nodes { + if node.Info().MasterReplOffset > 0 { + replIds[node.Info().MasterReplId] = struct{}{} + } + } + if len(replIds) == 1 { + slices.SortStableFunc(nodes, func(i, j redis.RedisNode) int { + if i.Info().MasterReplOffset > j.Info().MasterReplOffset { + return -1 + } + return 1 + }) + masterCandidate = nodes[0] + } + } + if masterCandidate == nil { + slices.SortStableFunc(nodes, func(i, j redis.RedisNode) int { + if i.Info().UptimeInSeconds > j.Info().UptimeInSeconds { + return -1 + } + return 1 + }) + masterCandidate = nodes[0] + } + } + + if masterCandidate != nil { + if err := masterCandidate.ReplicaOf(ctx, "no", "one"); err != nil { + return err + } + time.Sleep(time.Second) + if err := s.Monitor(ctx, masterCandidate); err != nil { + s.logger.Error(err, "monitor failed", "node", masterCandidate.GetName()) + return err + } + masterIP := masterCandidate.DefaultIP().String() + masterPort := masterCandidate.Port() + for _, node := range s.failover.Nodes() { + if node.DefaultIP().String() == masterIP && node.Port() == masterPort { + continue + } + if err := node.ReplicaOf(ctx, masterIP, strconv.Itoa(masterPort)); err != nil { + s.logger.Error(err, "replicaof failed", "node", node.GetName(), "master", masterCandidate.GetName()) + } + } + } + return fmt.Errorf("No available node to failover") +} + +func (s *ManualMonitor) Monitor(ctx context.Context, masterNode redis.RedisNode) error { + masterAddr := net.JoinHostPort(masterNode.DefaultIP().String(), fmt.Sprintf("%d", masterNode.Port())) + if oldCm, err := s.client.GetConfigMap(ctx, s.failover.GetNamespace(), s.resourceName); errors.IsNotFound(err) { + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.resourceName, + Namespace: s.failover.GetNamespace(), + Labels: s.failover.GetLabels(), + OwnerReferences: util.BuildOwnerReferences(s.failover.Definition()), + }, + Data: map[string]string{ + "master": masterAddr, + }, + } + if err := s.client.CreateConfigMap(ctx, s.failover.GetNamespace(), &cm); err != nil { + return err + } + } else if err != nil { + return err + } else if oldCm.Data["master"] != masterAddr { + oldCm.Data["master"] = masterAddr + if err := s.client.UpdateConfigMap(ctx, s.failover.GetNamespace(), oldCm); err != nil { + return err + } + } + return nil +} diff --git a/internal/redis/failover/monitor/monitor.go b/internal/redis/failover/monitor/monitor.go new file mode 100644 index 0000000..422e957 --- /dev/null +++ b/internal/redis/failover/monitor/monitor.go @@ -0,0 +1,45 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package monitor + +import ( + "context" + "fmt" + + v1 "github.com/alauda/redis-operator/api/databases/v1" + clientset "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/go-logr/logr" +) + +func LoadFailoverMonitor(ctx context.Context, client clientset.ClientSet, inst types.RedisFailoverInstance, logger logr.Logger) (types.FailoverMonitor, error) { + def := inst.Definition() + if def.Status.Monitor.Policy == v1.ManualFailoverPolicy { + mon, err := NewManualMonitor(ctx, client, inst) + if err != nil { + logger.Error(err, "load manual monitor failed") + } + return mon, nil + } else if def.Status.Monitor.Policy == v1.SentinelFailoverPolicy { + repl, err := NewSentinelMonitor(ctx, client, inst) + if err != nil { + logger.Error(err, "load sentinel monitor failed") + } + return repl, nil + } + return nil, fmt.Errorf("unknown monitor policy %s", def.Status.Monitor.Policy) +} diff --git a/internal/redis/failover/monitor/sentinel_monitor.go b/internal/redis/failover/monitor/sentinel_monitor.go new file mode 100644 index 0000000..8273079 --- /dev/null +++ b/internal/redis/failover/monitor/sentinel_monitor.go @@ -0,0 +1,416 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package monitor + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "slices" + "strconv" + "strings" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/util" + clientset "github.com/alauda/redis-operator/pkg/kubernetes" + rediscli "github.com/alauda/redis-operator/pkg/redis" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" + "github.com/samber/lo" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + ErrNoMaster = fmt.Errorf("no master") + ErrMultipleMaster = fmt.Errorf("multiple master without majority agreement") + ErrNotEnoughNodes = fmt.Errorf("not enough sentinel nodes") +) + +var _ types.FailoverMonitor = (*SentinelMonitor)(nil) + +func IsMonitoringNodeOnline(node *rediscli.SentinelMonitorNode) bool { + if node == nil { + return false + } + return !strings.Contains(node.Flags, "down") && !strings.Contains(node.Flags, "disconnected") +} + +type SentinelMonitor struct { + client clientset.ClientSet + failover types.RedisFailoverInstance + groupName string + nodes []*SentinelNode + + logger logr.Logger +} + +func NewSentinelMonitor(ctx context.Context, k8scli clientset.ClientSet, inst types.RedisFailoverInstance) (*SentinelMonitor, error) { + if k8scli == nil { + return nil, fmt.Errorf("require clientset") + } + if inst == nil { + return nil, fmt.Errorf("require instance") + } + + monitor := &SentinelMonitor{ + client: k8scli, + failover: inst, + groupName: "mymaster", + logger: inst.Logger(), + } + var ( + username = inst.Definition().Status.Monitor.Username + passwords []string + tlsSecret = inst.Definition().Status.Monitor.TLSSecret + tlsConfig *tls.Config + monitorStatus = inst.Definition().Status.Monitor + ) + for _, passwordSecret := range []string{monitorStatus.PasswordSecret, monitorStatus.OldPasswordSecret} { + if passwordSecret == "" { + passwords = append(passwords, "") + continue + } + if secret, err := k8scli.GetSecret(ctx, inst.GetNamespace(), passwordSecret); err != nil { + obj := client.ObjectKey{Namespace: inst.GetNamespace(), Name: passwordSecret} + monitor.logger.Error(err, "get password secret failed", "target", obj) + return nil, err + } else { + passwords = append(passwords, string(secret.Data["password"])) + } + } + + if tlsSecret != "" { + if secret, err := k8scli.GetSecret(ctx, inst.GetNamespace(), tlsSecret); err != nil { + obj := client.ObjectKey{Namespace: inst.GetNamespace(), Name: tlsSecret} + monitor.logger.Error(err, "get tls secret failed", "target", obj) + return nil, err + } else if tlsConfig, err = util.LoadCertConfigFromSecret(secret); err != nil { + monitor.logger.Error(err, "load cert config failed") + return nil, err + } + } + for _, node := range inst.Definition().Status.Monitor.Nodes { + var ( + err error + snode *SentinelNode + ) + for _, password := range passwords { + addr := net.JoinHostPort(node.IP, fmt.Sprintf("%d", node.Port)) + if snode, err = NewSentinelNode(ctx, addr, username, password, tlsConfig); err != nil { + if strings.Contains(err.Error(), "NOAUTH Authentication required") || + strings.Contains(err.Error(), "invalid password") || + strings.Contains(err.Error(), "Client sent AUTH, but no password is set") || + strings.Contains(err.Error(), "invalid username-password pair") { + + monitor.logger.Error(err, "sentinel node auth failed, try old password", "addr", addr) + continue + } + monitor.logger.Error(err, "create sentinel node failed", "addr", addr) + } + break + } + if snode != nil { + monitor.nodes = append(monitor.nodes, snode) + } + } + return monitor, nil +} + +func (s *SentinelMonitor) Policy() databasesv1.FailoverPolicy { + return databasesv1.SentinelFailoverPolicy +} + +func (s *SentinelMonitor) Master(ctx context.Context) (*rediscli.SentinelMonitorNode, error) { + if s == nil { + return nil, nil + } + type Stat struct { + Node *rediscli.SentinelMonitorNode + Count int + } + var ( + masterStat []*Stat + registeredNodes int + ) + for _, node := range s.nodes { + n, err := node.MonitoringMaster(ctx, s.groupName) + if err != nil { + // NOTE: here ignored any error, for the node may be offline forever + s.logger.Error(err, "check monitor status of sentinel failed", "addr", node.addr) + continue + } + registeredNodes += 1 + if i := slices.IndexFunc(masterStat, func(s *Stat) bool { + // NOTE: here cannot use runid to identify the node, + // for the same node may have different ip and port after quick restarted with a different addr + if s.Node.IP == n.IP && s.Node.Port == n.Port { + s.Count++ + return true + } + return false + }); i < 0 { + masterStat = append(masterStat, &Stat{Node: n, Count: 1}) + } + } + if len(masterStat) == 0 { + return nil, ErrNoMaster + } + slices.SortStableFunc(masterStat, func(i, j *Stat) int { + if i.Count >= j.Count { + return -1 + } + return 1 + }) + + if masterStat[0].Count >= 1+len(s.nodes)/2 || masterStat[0].Count == registeredNodes { + return masterStat[0].Node, nil + } + return nil, ErrMultipleMaster +} + +func (s *SentinelMonitor) Replicas(ctx context.Context) ([]*rediscli.SentinelMonitorNode, error) { + if s == nil { + return nil, nil + } + + var nodes []*rediscli.SentinelMonitorNode + for _, node := range s.nodes { + ns, err := node.MonitoringReplicas(ctx, s.groupName) + if err == ErrNoMaster { + continue + } else if err != nil { + s.logger.Error(err, "check monitor status of sentinel failed", "addr", node.addr) + continue + } + for _, n := range ns { + if i := slices.IndexFunc(nodes, func(smn *rediscli.SentinelMonitorNode) bool { + // NOTE: here cannot use runid to identify the node, + // for the same node may have different ip and port after quick restarted with a different addr + return smn.IP == n.IP && smn.Port == n.Port + }); i != -1 { + nodes[i] = n + } else { + nodes = append(nodes, n) + } + } + } + return nodes, nil +} + +func (s *SentinelMonitor) Inited(ctx context.Context) (bool, error) { + if s == nil || len(s.nodes) == 0 { + return false, fmt.Errorf("no sentinel nodes") + } + + for _, node := range s.nodes { + if masterNode, err := node.MonitoringMaster(ctx, s.groupName); err == ErrNoMaster { + return false, nil + } else if err != nil { + return false, err + } else if !IsMonitoringNodeOnline(masterNode) { + return false, nil + } + } + return true, nil +} + +// AllNodeMonitored checks if all sentinel nodes are monitoring all the master and replicas +func (s *SentinelMonitor) AllNodeMonitored(ctx context.Context) (bool, error) { + if s == nil || len(s.nodes) == 0 { + return false, fmt.Errorf("no sentinel nodes") + } + + var ( + registeredNodes = map[string]struct{}{} + masters = map[string]int{} + ) + for _, node := range s.nodes { + if master, err := node.MonitoringMaster(ctx, s.groupName); err != nil { + if err == ErrNoMaster { + return false, nil + } + } else if IsMonitoringNodeOnline(master) { + registeredNodes[master.Address()] = struct{}{} + masters[master.Address()] += 1 + } else { + s.logger.Error(fmt.Errorf("master node offline"), "master node offline", "node", master.Address()) + return false, nil + } + + if replicas, err := node.MonitoringReplicas(ctx, s.groupName); err != nil { + if err == ErrNoMaster { + return false, nil + } + } else { + for _, replica := range replicas { + if IsMonitoringNodeOnline(replica) { + registeredNodes[replica.Address()] = struct{}{} + } + } + } + } + for _, node := range s.failover.Nodes() { + if !node.IsReady() { + s.logger.Info("node not ready, ignored", "node", node.GetName()) + continue + } + addr := net.JoinHostPort(node.DefaultIP().String(), strconv.Itoa(node.Port())) + addr2 := net.JoinHostPort(node.DefaultInternalIP().String(), strconv.Itoa(node.InternalPort())) + _, ok := registeredNodes[addr] + _, ok2 := registeredNodes[addr2] + if !ok && !ok2 { + return false, nil + } + } + if len(masters) > 1 { + return false, ErrMultipleMaster + } + return true, nil +} + +func (s *SentinelMonitor) UpdateConfig(ctx context.Context, params map[string]string) error { + if s == nil || len(s.nodes) == 0 { + return fmt.Errorf("no sentinel nodes") + } + logger := s.logger.WithName("UpdateConfig") + + for _, node := range s.nodes { + masterNode, err := node.MonitoringMaster(ctx, s.groupName) + if err != nil { + if err == ErrNoMaster || strings.HasSuffix(err.Error(), "no such host") { + continue + } + logger.Error(err, "check monitoring master failed") + return err + } + + needUpdatedConfigs := map[string]string{} + for k, v := range params { + switch k { + case "down-after-milliseconds": + if v != fmt.Sprintf("%d", masterNode.DownAfterMilliseconds) { + needUpdatedConfigs[k] = v + } + case "failover-timeout": + if v != fmt.Sprintf("%d", masterNode.FailoverTimeout) { + needUpdatedConfigs[k] = v + } + case "parallel-syncs": + if v != fmt.Sprintf("%d", masterNode.ParallelSyncs) { + needUpdatedConfigs[k] = v + } + case "auth-pass", "auth-user": + needUpdatedConfigs[k] = v + } + } + if len(needUpdatedConfigs) > 0 { + logger.Info("update configs", "node", node.addr, "configs", lo.Keys(params)) + if err := node.UpdateConfig(ctx, s.groupName, needUpdatedConfigs); err != nil { + logger.Error(err, "update sentinel monitor configs failed", "node", node.addr) + return err + } + } + } + return nil +} + +func (s *SentinelMonitor) Failover(ctx context.Context) error { + if s == nil || len(s.nodes) == 0 { + return fmt.Errorf("no sentinel nodes") + } + logger := s.logger.WithName("failover") + + // check most sentinel nodes available + var availableNodes []*SentinelNode + for _, node := range s.nodes { + if node.IsReady() { + availableNodes = append(availableNodes, node) + } + } + if len(availableNodes) < 1+len(s.nodes)/2 { + logger.Error(ErrNotEnoughNodes, "failover failed") + return ErrNotEnoughNodes + } + if err := availableNodes[0].Failover(ctx, s.groupName); err != nil { + logger.Error(err, "failover failed on node", "addr", availableNodes[0].addr) + return err + } + return nil +} + +// Monitor monitors the redis master node on the sentinel nodes +func (s *SentinelMonitor) Monitor(ctx context.Context, masterNode redis.RedisNode) error { + if s == nil || len(s.nodes) == 0 { + return fmt.Errorf("no monitor") + } + logger := s.logger.WithName("monitor") + + quorum := 1 + len(s.nodes)/2 + if s.failover.Definition().Spec.Sentinel.Quorum != nil { + quorum = int(*s.failover.Definition().Spec.Sentinel.Quorum) + } + + configs := map[string]string{ + "down-after-milliseconds": "30000", + "failover-timeout": "180000", + "parallel-syncs": "1", + } + for k, v := range s.failover.Definition().Spec.Sentinel.MonitorConfig { + configs[k] = v + } + + opUser := s.failover.Users().GetOpUser() + configs["auth-pass"] = opUser.Password.String() + if s.failover.Version().IsACLSupported() { + configs["auth-user"] = opUser.Name + } + masterIP, masterPort := masterNode.DefaultIP().String(), strconv.Itoa(masterNode.Port()) + for _, node := range s.nodes { + if master, err := node.MonitoringMaster(ctx, s.groupName); err == ErrNoMaster || + (master != nil && (master.IP != masterIP || master.Port != masterPort || master.Quorum != int32(quorum))) || + !IsMonitoringNodeOnline(master) { + + if err := node.Monitor(ctx, s.groupName, masterIP, masterPort, quorum, configs); err != nil { + logger.Error(err, "monitor failed on node", "addr", net.JoinHostPort(masterIP, masterPort)) + } + } else if master != nil && master.IP == masterIP && master.Port == masterPort { + needUpdate := false + _NEED_UPDATE_: + for k, v := range configs { + switch k { + case "down-after-milliseconds": + needUpdate = v != fmt.Sprintf("%d", master.DownAfterMilliseconds) + break _NEED_UPDATE_ + case "failover-timeout": + needUpdate = v != fmt.Sprintf("%d", master.FailoverTimeout) + break _NEED_UPDATE_ + case "parallel-syncs": + needUpdate = v != fmt.Sprintf("%d", master.ParallelSyncs) + break _NEED_UPDATE_ + } + } + if needUpdate { + if err := node.UpdateConfig(ctx, s.groupName, configs); err != nil { + logger.Error(err, "update config failed on node", "addr", net.JoinHostPort(masterIP, masterPort)) + } + } + } + } + return nil +} diff --git a/internal/redis/failover/monitor/sentinel_node.go b/internal/redis/failover/monitor/sentinel_node.go new file mode 100644 index 0000000..ee4679b --- /dev/null +++ b/internal/redis/failover/monitor/sentinel_node.go @@ -0,0 +1,182 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package monitor + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "strings" + "time" + + "github.com/alauda/redis-operator/pkg/redis" + rediscli "github.com/alauda/redis-operator/pkg/redis" +) + +func NewSentinelNode(ctx context.Context, addr, username, password string, tlsConf *tls.Config) (*SentinelNode, error) { + client := rediscli.NewRedisClient(addr, rediscli.AuthConfig{ + Username: username, + Password: password, + TLSConfig: tlsConf, + }) + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + if err := client.Ping(ctx); err != nil { + return nil, err + } + return &SentinelNode{addr: addr, client: client}, nil +} + +type SentinelNode struct { + addr string + client rediscli.RedisClient +} + +func (sn *SentinelNode) MonitoringMaster(ctx context.Context, name string) (*rediscli.SentinelMonitorNode, error) { + if sn == nil || sn.client == nil { + return nil, fmt.Errorf("no client") + } + + val, err := sn.client.Do(ctx, "SENTINEL", "MASTER", name) + if err != nil { + if strings.Contains(err.Error(), "No such master with that name") { + return nil, ErrNoMaster + } + return nil, err + } + return rediscli.ParseSentinelMonitorNode(val), nil +} + +func (sn *SentinelNode) MonitoringReplicas(ctx context.Context, name string) ([]*rediscli.SentinelMonitorNode, error) { + if sn == nil || sn.client == nil { + return nil, fmt.Errorf("no client") + } + + var ( + replicas []*rediscli.SentinelMonitorNode + ) + + if vals, err := redis.Values(sn.client.Do(ctx, "SENTINEL", "REPLICAS", name)); err != nil { + if strings.Contains(err.Error(), "No such master with that name") { + return nil, ErrNoMaster + } + return nil, err + } else { + for _, val := range vals { + replicas = append(replicas, rediscli.ParseSentinelMonitorNode(val)) + } + } + return replicas, nil +} + +func (sn *SentinelNode) UpdateConfig(ctx context.Context, name string, params map[string]string) error { + if sn == nil || sn.client == nil { + return fmt.Errorf("no client") + } + + cmds := [][]any{} + for k, v := range params { + cmds = append(cmds, []any{"SENTINEL", "SET", name, k, v}) + } + cmds = append(cmds, []any{"SENTINEL", "RESET", name}) + rets, err := sn.client.Pipeline(ctx, cmds) + if err != nil { + return err + } + for _, ret := range rets { + if ret.Error != nil { + if strings.Contains(err.Error(), "No such master with that name") { + err = errors.Join(err, ErrNoMaster) + } else { + err = errors.Join(err, ret.Error) + } + } + } + return err +} + +func (sn *SentinelNode) Monitor(ctx context.Context, name, ip, port string, quorum int, params map[string]string) error { + if sn == nil || sn.client == nil { + return fmt.Errorf("no client") + } + + if _, err := sn.client.Do(ctx, "SENTINEL", "REMOVE", name); err != nil { + // ignore error if no such master + if !strings.Contains(err.Error(), "No such master with that name") { + return err + } + } + + cmds := [][]any{{"SENTINEL", "MONITOR", name, ip, port, quorum}} + for k, v := range params { + cmds = append(cmds, []any{"SENTINEL", "SET", name, k, v}) + } + cmds = append(cmds, []any{"SENTINEL", "RESET", name}) + + rets, err := sn.client.Pipeline(ctx, cmds) + if err != nil { + return err + } + for _, ret := range rets { + if ret.Error != nil { + err = errors.Join(err, ret.Error) + } + } + return err +} + +func (sn *SentinelNode) Failover(ctx context.Context, name string) error { + if sn == nil || sn.client == nil { + return fmt.Errorf("no client") + } + + masterNode, err := sn.MonitoringMaster(ctx, name) + if err != nil { + return err + } + var ( + masterAddr = masterNode.Address() + currentMasterAddr string + ) + if _, err := sn.client.Do(ctx, "SENTINEL", "FAILOVER", name); err != nil { + return err + } + for j := 0; j < 10; j++ { + time.Sleep(3 * time.Second) + if currentNode, err := sn.MonitoringMaster(ctx, name); err != nil { + return err + } else { + currentMasterAddr = currentNode.Address() + if currentMasterAddr != masterAddr { + return nil + } + } + } + return fmt.Errorf("failover timeout, old master addr %s, current master addr %s", masterAddr, currentMasterAddr) +} + +func (sn *SentinelNode) IsReady() bool { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + if err := sn.client.Ping(ctx); err != nil { + return false + } + return true +} diff --git a/internal/redis/failover/redisfailover.go b/internal/redis/failover/redisfailover.go new file mode 100644 index 0000000..428ad06 --- /dev/null +++ b/internal/redis/failover/redisfailover.go @@ -0,0 +1,880 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package failover + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "reflect" + "slices" + "strconv" + "strings" + + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + redisv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/internal/redis/failover/monitor" + "github.com/alauda/redis-operator/internal/util" + clientset "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/security/acl" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/pkg/types/user" + + "github.com/go-logr/logr" + "github.com/samber/lo" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ types.RedisFailoverInstance = (*RedisFailover)(nil) + +type RedisFailover struct { + databasesv1.RedisFailover + + client clientset.ClientSet + eventRecorder record.EventRecorder + + users acl.Users + tlsConfig *tls.Config + configmap map[string]string + replication types.RedisReplication + monitor types.FailoverMonitor + + redisUsers []*redisv1.RedisUser + + logger logr.Logger +} + +func NewRedisFailover(ctx context.Context, k8sClient clientset.ClientSet, eventRecorder record.EventRecorder, def *databasesv1.RedisFailover, logger logr.Logger) (*RedisFailover, error) { + inst := &RedisFailover{ + RedisFailover: *def, + client: k8sClient, + eventRecorder: eventRecorder, + configmap: make(map[string]string), + logger: logger.WithName("F").WithValues("instance", client.ObjectKeyFromObject(def).String()), + } + var err error + if inst.users, err = inst.loadUsers(ctx); err != nil { + inst.logger.Error(err, "load user failed") + return nil, err + } + if inst.tlsConfig, err = inst.loadTLS(ctx); err != nil { + inst.logger.Error(err, "loads tls failed") + return nil, err + } + + if inst.replication, err = LoadRedisReplication(ctx, k8sClient, inst, inst.logger); err != nil { + inst.logger.Error(err, "load replicas failed") + return nil, err + } + if inst.monitor, err = monitor.LoadFailoverMonitor(ctx, k8sClient, inst, inst.logger); err != nil { + inst.logger.Error(err, "load monitor failed") + return nil, err + } + + if inst.Version().IsACLSupported() { + inst.LoadRedisUsers(ctx) + } + return inst, nil +} + +func (s *RedisFailover) NamespacedName() client.ObjectKey { + if s == nil { + return client.ObjectKey{} + } + return client.ObjectKey{Namespace: s.GetNamespace(), Name: s.GetName()} +} + +func (s *RedisFailover) Arch() redis.RedisArch { + return core.RedisSentinel +} + +func (s *RedisFailover) UpdateStatus(ctx context.Context, st types.InstanceStatus, msg string) error { + if s == nil { + return nil + } + + var ( + err error + replicas = int32(len(s.Nodes())) + nodeports = map[int32]struct{}{} + rs = lo.IfF(s != nil && s.replication != nil, func() *appsv1.StatefulSetStatus { + return s.replication.Status() + }).Else(nil) + sentinel *databasesv1.RedisSentinel + cr = s.Definition() + status = cr.Status.DeepCopy() + ) + + switch st { + case types.OK: + status.Phase = databasesv1.Ready + case types.Fail: + status.Phase = databasesv1.Fail + case types.Paused: + status.Phase = databasesv1.Paused + default: + status.Phase = "" + } + status.Message = msg + + if s.IsBindedSentinel() { + if sentinel, err = s.client.GetRedisSentinel(ctx, s.GetNamespace(), s.GetName()); err != nil && !errors.IsNotFound(err) { + s.logger.Error(err, "get RedisSentinel failed") + return err + } + } + + // collect instance statistics + status.Nodes = status.Nodes[:0] + detailedNodes := []core.RedisDetailedNode{} + for _, node := range s.Nodes() { + detailedNode := core.RedisDetailedNode{ + RedisNode: core.RedisNode{ + Role: node.Role(), + MasterRef: node.MasterID(), + IP: node.DefaultIP().String(), + Port: fmt.Sprintf("%d", node.Port()), + PodName: node.GetName(), + StatefulSet: s.replication.GetName(), + NodeName: node.NodeIP().String(), + }, + Version: node.Info().RedisVersion, + UsedMemory: node.Info().UsedMemory, + UsedMemoryDataset: node.Info().UsedMemoryDataset, + } + status.Nodes = append(status.Nodes, detailedNode.RedisNode) + detailedNodes = append(detailedNodes, detailedNode) + if node.Role() == core.RedisRoleMaster { + status.Master.Name = node.GetName() + status.Master.Address = net.JoinHostPort(node.DefaultIP().String(), fmt.Sprintf("%d", node.Port())) + status.Master.Status = databasesv1.RedisStatusMasterOK + } + if port := node.Definition().Labels[builder.PodAnnouncePortLabelKey]; port != "" { + val, _ := strconv.ParseInt(port, 10, 32) + nodeports[int32(val)] = struct{}{} + } + } + + status.Instance.Redis.Size = replicas + status.Instance.Redis.Ready = 0 + for _, node := range s.Nodes() { + if node.IsReady() { + status.Instance.Redis.Ready++ + } + } + + if s.Monitor() != nil { + masterNode, err := s.Monitor().Master(ctx) + if err != nil { + if err != monitor.ErrNoMaster { + s.logger.Error(err, "get master failed") + } + status.Master.Address = "" + status.Master.Status = databasesv1.RedisStatusMasterDown + } else if masterNode != nil { + status.Master.Address = masterNode.Address() + status.Master.Address = masterNode.Address() + if masterNode.Flags == "master" { + status.Master.Status = databasesv1.RedisStatusMasterOK + } else { + status.Master.Status = databasesv1.RedisStatusMasterDown + } + } + status.Master.Name = s.RedisFailover.Status.Monitor.Name + } + + phase, msg := func() (databasesv1.Phase, string) { + // use passed status if provided + if status.Phase == databasesv1.Fail || status.Phase == databasesv1.Paused { + return status.Phase, status.Message + } + + if sentinel != nil { + switch sentinel.Status.Phase { + case databasesv1.SentinelCreating: + return databasesv1.Creating, sentinel.Status.Message + case databasesv1.SentinelFail: + return databasesv1.Fail, sentinel.Status.Message + } + } + + // check creating + if rs == nil || rs.CurrentReplicas != s.Definition().Spec.Redis.Replicas || + rs.Replicas != s.Definition().Spec.Redis.Replicas { + return databasesv1.Creating, "" + } + + var pendingPods []string + // check pending + for _, node := range s.Nodes() { + for _, cond := range node.Definition().Status.Conditions { + if cond.Type == corev1.PodScheduled && cond.Status == corev1.ConditionFalse { + pendingPods = append(pendingPods, node.GetName()) + } + } + } + if len(pendingPods) > 0 { + return databasesv1.Pending, fmt.Sprintf("pods %s pending", strings.Join(pendingPods, ",")) + } + + // check nodeport applied + if seq := s.Spec.Redis.Expose.NodePortSequence; s.Spec.Redis.Expose.ServiceType == corev1.ServiceTypeNodePort { + var ( + notAppliedPorts = []string{} + customPorts, _ = helper.ParseSequencePorts(seq) + ) + for _, port := range customPorts { + if _, ok := nodeports[port]; !ok { + notAppliedPorts = append(notAppliedPorts, strconv.Itoa(int(port))) + } + } + if len(notAppliedPorts) > 0 { + return databasesv1.WaitingPodReady, fmt.Sprintf("nodeport %s not applied", strings.Join(notAppliedPorts, ",")) + } + } + + var notReadyPods []string + for _, node := range s.Nodes() { + if !node.IsReady() { + notReadyPods = append(notReadyPods, node.GetName()) + } + } + if len(notReadyPods) > 0 { + return databasesv1.WaitingPodReady, fmt.Sprintf("pods %s not ready", strings.Join(notReadyPods, ",")) + } + + if isAllMonitored, _ := s.Monitor().AllNodeMonitored(ctx); !isAllMonitored { + return databasesv1.Creating, "not all nodes monitored" + } + + // make sure all is ready + if (rs != nil && + rs.ReadyReplicas == s.Definition().Spec.Redis.Replicas && + rs.CurrentReplicas == rs.ReadyReplicas && + rs.CurrentRevision == rs.UpdateRevision) && + // sentinel + (sentinel == nil || sentinel.Status.Phase == databasesv1.SentinelReady) && + // instance status + (status.Master.Status == databasesv1.RedisStatusMasterOK) { + + return databasesv1.Ready, "" + } + return databasesv1.WaitingPodReady, "" + }() + status.Phase, status.Message = phase, lo.If(msg == "", status.Message).Else(msg) + + // update detailed configmap + detailStatus := databasesv1.RedisFailoverDetailedStatus{ + Phase: status.Phase, + Message: status.Message, + Nodes: detailedNodes, + } + detailStatusCM, _ := failoverbuilder.NewRedisFailoverDetailedStatusConfigMap(s, &detailStatus) + if oldCm, err := s.client.GetConfigMap(ctx, s.GetNamespace(), detailStatusCM.GetName()); errors.IsNotFound(err) { + if err := s.client.CreateConfigMap(ctx, s.GetNamespace(), detailStatusCM); err != nil { + s.logger.Error(err, "create detailed status configmap failed") + } + } else if err != nil { + s.logger.Error(err, "get detailed status configmap failed") + } else if failoverbuilder.ShouldUpdateDetailedStatusConfigMap(oldCm, &detailStatus) { + if err := s.client.UpdateConfigMap(ctx, s.GetNamespace(), detailStatusCM); err != nil { + s.logger.Error(err, "update detailed status configmap failed") + } + } + if status.DetailedStatusRef == nil { + status.DetailedStatusRef = &corev1.ObjectReference{ + Kind: "ConfigMap", + Name: detailStatusCM.GetName(), + } + } + // update status + s.RedisFailover.Status = *status + if err := s.client.UpdateRedisFailoverStatus(ctx, &s.RedisFailover); err != nil { + s.logger.Error(err, "update RedisFailover status failed") + return err + } + return nil +} + +func (s *RedisFailover) Definition() *databasesv1.RedisFailover { + if s == nil { + return nil + } + return &s.RedisFailover +} + +func (s *RedisFailover) Version() redis.RedisVersion { + if s == nil { + return redis.RedisVersionUnknown + } + + if version, err := redis.ParseRedisVersionFromImage(s.Spec.Redis.Image); err != nil { + s.logger.Error(err, "parse redis version failed") + return redis.RedisVersionUnknown + } else { + return version + } +} + +func (s *RedisFailover) Masters() []redis.RedisNode { + if s == nil || s.replication == nil { + return nil + } + var ret []redis.RedisNode + for _, v := range s.replication.Nodes() { + if v.Role() == core.RedisRoleMaster { + ret = append(ret, v) + } + } + return ret +} + +func (s *RedisFailover) Nodes() []redis.RedisNode { + if s == nil || s.replication == nil { + return nil + } + return append([]redis.RedisNode{}, s.replication.Nodes()...) +} + +func (s *RedisFailover) RawNodes(ctx context.Context) ([]corev1.Pod, error) { + if s == nil { + return nil, nil + } + name := failoverbuilder.GetFailoverStatefulSetName(s.GetName()) + sts, err := s.client.GetStatefulSet(ctx, s.GetNamespace(), name) + if err != nil { + if errors.IsNotFound(err) { + return nil, nil + } + s.logger.Error(err, "load statefulset failed", "name", name) + return nil, err + } + // load pods by statefulset selector + ret, err := s.client.GetStatefulSetPodsByLabels(ctx, sts.GetNamespace(), sts.Spec.Selector.MatchLabels) + if err != nil { + s.logger.Error(err, "loads pods of shard failed") + return nil, err + } + return ret.Items, nil +} + +func (s *RedisFailover) IsInService() bool { + if s == nil || s.Monitor() == nil { + return false + } + + master, err := s.Monitor().Master(context.TODO()) + if err == monitor.ErrNoMaster || err == monitor.ErrMultipleMaster { + return false + } else if err != nil { + s.logger.Error(err, "get master failed") + return false + } else if master != nil && master.Flags == "master" { + return true + } + return false +} + +func (s *RedisFailover) IsReady() bool { + if s == nil { + return false + } + if s.RedisFailover.Status.Phase == databasesv1.Ready { + return true + } + return false +} + +func (s *RedisFailover) Users() (us acl.Users) { + if s == nil { + return nil + } + + // clone before return + for _, user := range s.users { + u := *user + if u.Password != nil { + p := *u.Password + u.Password = &p + } + us = append(us, &u) + } + return +} + +func (s *RedisFailover) TLSConfig() *tls.Config { + if s == nil { + return nil + } + return s.tlsConfig +} + +func (s *RedisFailover) Restart(ctx context.Context, annotationKeyVal ...string) error { + if s == nil || s.replication == nil { + return nil + } + return s.replication.Restart(ctx, annotationKeyVal...) +} + +func (s *RedisFailover) Refresh(ctx context.Context) error { + if s == nil { + return nil + } + logger := s.logger.WithName("Refresh") + logger.V(3).Info("refreshing sentinel", "target", util.ObjectKey(s.GetNamespace(), s.GetName())) + + // load cr + var cr databasesv1.RedisFailover + if err := retry.OnError(retry.DefaultRetry, func(err error) bool { + if errors.IsInternalError(err) || + errors.IsServerTimeout(err) || + errors.IsTimeout(err) || + errors.IsTooManyRequests(err) || + errors.IsServiceUnavailable(err) { + return true + } + return false + }, func() error { + return s.client.Client().Get(ctx, client.ObjectKeyFromObject(&s.RedisFailover), &cr) + }); err != nil { + if errors.IsNotFound(err) { + return nil + } + logger.Error(err, "get RedisFailover failed") + return err + } + if cr.Name == "" { + return fmt.Errorf("RedisFailover is nil") + } + s.RedisFailover = cr + err := s.RedisFailover.Validate() + if err != nil { + return err + } + + if s.users, err = s.loadUsers(ctx); err != nil { + logger.Error(err, "load users failed") + return err + } + + if s.replication, err = LoadRedisReplication(ctx, s.client, s, logger); err != nil { + logger.Error(err, "load replicas failed") + return err + } + return nil +} + +func (s *RedisFailover) LoadRedisUsers(ctx context.Context) { + oldOpUser, _ := s.client.GetRedisUser(ctx, s.GetNamespace(), failoverbuilder.GenerateFailoverOperatorsRedisUserName(s.GetName())) + oldDefultUser, _ := s.client.GetRedisUser(ctx, s.GetNamespace(), failoverbuilder.GenerateFailoverDefaultRedisUserName(s.GetName())) + s.redisUsers = []*redisv1.RedisUser{oldOpUser, oldDefultUser} +} + +func (s *RedisFailover) loadUsers(ctx context.Context) (acl.Users, error) { + var ( + name = failoverbuilder.GenerateFailoverACLConfigMapName(s.GetName()) + users acl.Users + ) + + if s.Version().IsACLSupported() { + getPassword := func(secretName string) (*user.Password, error) { + if secret, err := s.loadUserSecret(ctx, client.ObjectKey{ + Namespace: s.GetNamespace(), + Name: secretName, + }); err != nil { + return nil, err + } else { + if password, err := user.NewPassword(secret); err != nil { + return nil, err + } else { + return password, nil + } + } + } + for _, name := range []string{ + failoverbuilder.GenerateFailoverOperatorsRedisUserName(s.GetName()), + failoverbuilder.GenerateFailoverDefaultRedisUserName(s.GetName()), + } { + if ru, err := s.client.GetRedisUser(ctx, s.GetNamespace(), name); err != nil { + s.logger.Error(err, "load operator user failed") + users = nil + break + } else { + var password *user.Password + if len(ru.Spec.PasswordSecrets) > 0 { + if password, err = getPassword(ru.Spec.PasswordSecrets[0]); err != nil { + s.logger.Error(err, "load operator user password failed") + return nil, err + } + } + if u, err := user.NewUserFromRedisUser(ru.Spec.Username, ru.Spec.AclRules, password); err != nil { + s.logger.Error(err, "load operator user failed") + return nil, err + } else { + users = append(users, u) + } + } + } + } + if len(users) == 0 { + if cm, err := s.client.GetConfigMap(ctx, s.GetNamespace(), name); errors.IsNotFound(err) { + var ( + username string + passwordSecret string + secret *corev1.Secret + ) + statefulSetName := failoverbuilder.GetFailoverStatefulSetName(s.GetName()) + sts, err := s.client.GetStatefulSet(ctx, s.GetNamespace(), statefulSetName) + if err != nil { + if !errors.IsNotFound(err) { + s.logger.Error(err, "load statefulset failed", "target", util.ObjectKey(s.GetNamespace(), s.GetName())) + } + if s.Version().IsACLSupported() { + passwordSecret = failoverbuilder.GenerateFailoverACLOperatorSecretName(s.GetName()) + username = user.DefaultOperatorUserName + } + } else { + spec := sts.Spec.Template.Spec + if container := util.GetContainerByName(&spec, failoverbuilder.ServerContainerName); container != nil { + for _, env := range container.Env { + if env.Name == failoverbuilder.PasswordENV && env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { + passwordSecret = env.ValueFrom.SecretKeyRef.LocalObjectReference.Name + } else if env.Name == failoverbuilder.OperatorSecretName && env.Value != "" { + passwordSecret = env.Value + } else if env.Name == failoverbuilder.OperatorUsername { + username = env.Value + } + } + } + if passwordSecret == "" { + // COMPAT: for old sentinel version, the secret is mounted to the pod + for _, vol := range spec.Volumes { + if vol.Name == "redis-auth" && vol.Secret != nil { + passwordSecret = vol.Secret.SecretName + break + } + } + } + } + + if passwordSecret != "" { + objKey := client.ObjectKey{Namespace: s.GetNamespace(), Name: passwordSecret} + if secret, err = s.loadUserSecret(ctx, objKey); err != nil { + s.logger.Error(err, "load user secret failed", "target", objKey) + return nil, err + } + } else if passwordSecret := s.Spec.Auth.SecretPath; passwordSecret != "" { + secret, err = s.client.GetSecret(ctx, s.GetNamespace(), passwordSecret) + if err != nil { + return nil, err + } + } + role := user.RoleDeveloper + if username == user.DefaultOperatorUserName { + role = user.RoleOperator + } else if username == "" { + username = user.DefaultUserName + } + + if role == user.RoleOperator { + if u, err := user.NewOperatorUser(secret, s.Version().IsACL2Supported()); err != nil { + s.logger.Error(err, "init users failed") + return nil, err + } else { + users = append(users, u) + } + + if passwordSecret := s.Spec.Auth.SecretPath; passwordSecret != "" { + secret, err = s.client.GetSecret(ctx, s.GetNamespace(), passwordSecret) + if err != nil { + return nil, err + } + u, _ := user.NewUser(user.DefaultUserName, user.RoleDeveloper, secret, s.Version().IsACL2Supported()) + users = append(users, u) + } else { + u, _ := user.NewUser(user.DefaultUserName, user.RoleDeveloper, nil, s.Version().IsACL2Supported()) + users = append(users, u) + } + } else { + if u, err := user.NewUser(username, role, secret, s.Version().IsACL2Supported()); err != nil { + s.logger.Error(err, "init users failed") + return nil, err + } else { + users = append(users, u) + } + } + } else if err != nil { + s.logger.Error(err, "load default users's password secret failed", "target", util.ObjectKey(s.GetNamespace(), name)) + return nil, err + } else if users, err = acl.LoadACLUsers(ctx, s.client, cm); err != nil { + s.logger.Error(err, "load acl failed") + return nil, err + } + } + + var ( + defaultUser = users.GetDefaultUser() + rule *user.Rule + ) + if len(defaultUser.Rules) > 0 { + rule = defaultUser.Rules[0] + } else { + rule = &user.Rule{} + } + if s.Version().IsACL2Supported() { + rule.Channels = []string{"*"} + } + + renameVal := s.Definition().Spec.Redis.CustomConfig[failoverbuilder.RedisConfig_RenameCommand] + renames, _ := clusterbuilder.ParseRenameConfigs(renameVal) + if len(renameVal) > 0 { + rule.DisallowedCommands = []string{} + for key, val := range renames { + if key != val && !slices.Contains(rule.DisallowedCommands, key) { + rule.DisallowedCommands = append(rule.DisallowedCommands, key) + } + } + } + defaultUser.Rules = append(defaultUser.Rules[0:0], rule) + + return users, nil +} + +func (s *RedisFailover) loadUserSecret(ctx context.Context, objKey client.ObjectKey) (*corev1.Secret, error) { + secret, err := s.client.GetSecret(ctx, objKey.Namespace, objKey.Name) + if err != nil && !errors.IsNotFound(err) { + s.logger.Error(err, "load default users's password secret failed", "target", objKey.String()) + return nil, err + } else if errors.IsNotFound(err) { + secret = failoverbuilder.NewFailoverOpSecret(s.Definition()) + err := s.client.CreateSecret(ctx, objKey.Namespace, secret) + if err != nil { + return nil, err + } + } else if _, ok := secret.Data[user.PasswordSecretKey]; !ok { + return nil, fmt.Errorf("no password found") + } + return secret, nil +} + +func (s *RedisFailover) loadTLS(ctx context.Context) (*tls.Config, error) { + if s == nil { + return nil, nil + } + logger := s.logger.WithName("loadTLS") + + secretName := s.Status.TLSSecret + if secretName == "" { + // load current tls secret. + // because previous cr not recorded the secret name, we should load it from statefulset + stsName := failoverbuilder.GetFailoverStatefulSetName(s.GetName()) + if sts, err := s.client.GetStatefulSet(ctx, s.GetNamespace(), stsName); err != nil { + s.logger.Error(err, "load statefulset failed", "target", util.ObjectKey(s.GetNamespace(), s.GetName())) + } else { + for _, vol := range sts.Spec.Template.Spec.Volumes { + if vol.Name == failoverbuilder.RedisTLSVolumeName { + secretName = vol.VolumeSource.Secret.SecretName + } + } + } + } + if secretName == "" { + return nil, nil + } + if secret, err := s.client.GetSecret(ctx, s.GetNamespace(), secretName); err != nil { + logger.Error(err, "secret not found", "name", secretName) + return nil, err + } else if cert, err := util.LoadCertConfigFromSecret(secret); err != nil { + logger.Error(err, "load cert config failed") + return nil, err + } else { + return cert, nil + } +} + +func (s *RedisFailover) IsBindedSentinel() bool { + if s == nil || s.RedisFailover.Spec.Sentinel == nil { + return false + } + return s.RedisFailover.Spec.Sentinel.SentinelReference == nil +} + +func (s *RedisFailover) Selector() map[string]string { + if s == nil { + return nil + } + if s.replication != nil && s.replication.Definition() != nil { + return s.replication.Definition().Spec.Selector.MatchLabels + } + return nil +} + +func (s *RedisFailover) IsACLUserExists() bool { + if !s.Version().IsACLSupported() { + return false + } + if len(s.redisUsers) == 0 { + return false + } + for _, v := range s.redisUsers { + if v == nil { + return false + } + } + return true +} + +func (s *RedisFailover) IsResourceFullfilled(ctx context.Context) (bool, error) { + var ( + serviceKey = corev1.SchemeGroupVersion.WithKind("Service") + stsKey = appsv1.SchemeGroupVersion.WithKind("StatefulSet") + sentinelKey = databasesv1.GroupVersion.WithKind("RedisSentinel") + ) + resources := map[schema.GroupVersionKind][]string{ + serviceKey: { + failoverbuilder.GetFailoverStatefulSetName(s.GetName()), // rfr- + failoverbuilder.GetRedisROServiceName(s.GetName()), // rfr--read-only + failoverbuilder.GetRedisRWServiceName(s.GetName()), // rfr--read-write + }, + stsKey: { + failoverbuilder.GetFailoverStatefulSetName(s.GetName()), + }, + sentinelKey: { + s.GetName(), + }, + } + + if s.Spec.Redis.Expose.ServiceType == corev1.ServiceTypeLoadBalancer || + s.Spec.Redis.Expose.ServiceType == corev1.ServiceTypeNodePort { + for i := 0; i < int(s.Spec.Redis.Replicas); i++ { + stsName := failoverbuilder.GetFailoverStatefulSetName(s.GetName()) + resources[serviceKey] = append(resources[serviceKey], fmt.Sprintf("%s-%d", stsName, i)) + } + } + + for gvk, names := range resources { + for _, name := range names { + var obj unstructured.Unstructured + obj.SetGroupVersionKind(gvk) + + err := s.client.Client().Get(ctx, client.ObjectKey{Namespace: s.GetNamespace(), Name: name}, &obj) + if errors.IsNotFound(err) { + s.logger.V(3).Info("resource not found", "kind", gvk.Kind, "target", util.ObjectKey(s.GetNamespace(), name)) + return false, nil + } else if err != nil { + s.logger.Error(err, "get resource failed", "kind", gvk.Kind, "target", util.ObjectKey(s.GetNamespace(), name)) + return false, err + } + } + } + + name := failoverbuilder.GetFailoverStatefulSetName(s.GetName()) + if sts, err := s.client.GetStatefulSet(ctx, s.GetNamespace(), name); err != nil { + s.logger.Error(err, "load statefulset failed", "target", util.ObjectKey(s.GetNamespace(), name)) + return false, err + } else { + redisToolsImage := config.GetRedisToolsImage(s) + if redisToolsImage == "" { + return false, fmt.Errorf("redis-tools image not found") + } + spec := sts.Spec.Template.Spec + for _, container := range append(append([]corev1.Container{}, spec.InitContainers...), spec.Containers...) { + if !strings.Contains(container.Image, "middleware/redis-tools") { + continue + } + if container.Image != redisToolsImage { + return false, nil + } + } + } + + { + // check sentinel + newSen := failoverbuilder.NewFailoverSentinel(s) + oldSen, err := s.client.GetRedisSentinel(ctx, s.GetNamespace(), s.GetName()) + if errors.IsNotFound(err) { + return false, nil + } else if err != nil { + s.logger.Error(err, "get sentinel failed", "target", client.ObjectKeyFromObject(newSen)) + return false, err + } + if !reflect.DeepEqual(newSen.Spec, oldSen.Spec) || + !reflect.DeepEqual(newSen.Labels, oldSen.Labels) || + !reflect.DeepEqual(newSen.Annotations, oldSen.Annotations) { + oldSen.Spec = newSen.Spec + oldSen.Labels = newSen.Labels + oldSen.Annotations = newSen.Annotations + return false, nil + } + } + return true, nil +} + +func (s *RedisFailover) IsACLAppliedToAll() bool { + if s == nil || !s.Version().IsACLSupported() { + return false + } + for _, node := range s.Nodes() { + if !node.CurrentVersion().IsACLSupported() || !node.IsACLApplied() { + return false + } + } + return true +} + +func (c *RedisFailover) Logger() logr.Logger { + if c == nil { + return logr.Discard() + } + return c.logger +} + +func (c *RedisFailover) SendEventf(eventtype, reason, messageFmt string, args ...interface{}) { + if c == nil { + return + } + c.eventRecorder.Eventf(c.Definition(), eventtype, reason, messageFmt, args...) +} + +func (s *RedisFailover) Monitor() types.FailoverMonitor { + if s == nil { + return nil + } + return s.monitor +} + +func (c *RedisFailover) IsStandalone() bool { + if c == nil { + return false + } + return c.RedisFailover.Spec.Sentinel == nil +} diff --git a/pkg/models/sentinel/replicas.go b/internal/redis/failover/replication.go similarity index 50% rename from pkg/models/sentinel/replicas.go rename to internal/redis/failover/replication.go index e1e91f3..9ddeabf 100644 --- a/pkg/models/sentinel/replicas.go +++ b/internal/redis/failover/replication.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package sentinel +package failover import ( "context" @@ -22,12 +22,13 @@ import ( "fmt" "time" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder/failoverbuilder" + model "github.com/alauda/redis-operator/internal/redis" + "github.com/alauda/redis-operator/internal/util" clientset "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/models" "github.com/alauda/redis-operator/pkg/types" "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" appv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -35,96 +36,102 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -var _ types.RedisSentinelReplica = (*RedisSentinelReplica)(nil) +var _ types.RedisReplication = (*RedisReplication)(nil) -type RedisSentinelReplica struct { +type RedisReplication struct { appv1.StatefulSet client clientset.ClientSet - sentinel types.RedisInstance + failover types.RedisFailoverInstance nodes []redis.RedisNode logger logr.Logger } -func LoadRedisSentinelReplicas(ctx context.Context, client clientset.ClientSet, sentinel types.RedisInstance, logger logr.Logger) ([]types.RedisSentinelReplica, error) { - - var shards []types.RedisSentinelReplica - name := sentinelbuilder.GetSentinelStatefulSetName(sentinel.GetName()) - sts, err := client.GetStatefulSet(ctx, sentinel.GetNamespace(), name) +func LoadRedisReplication(ctx context.Context, client clientset.ClientSet, inst types.RedisFailoverInstance, logger logr.Logger) (types.RedisReplication, error) { + name := failoverbuilder.GetFailoverStatefulSetName(inst.GetName()) + sts, err := client.GetStatefulSet(ctx, inst.GetNamespace(), name) if err != nil { if errors.IsNotFound(err) { - logger.Info("load statefulset not found", "name", name) - return shards, nil + return nil, nil } - logger.Info("load statefulset failed", "name", name) + logger.Error(err, "load statefulset failed", "name", name) return nil, err } - if node, err := NewRedisSentinelReplica(ctx, client, sentinel, sts, logger); err != nil { + repl, err := NewRedisReplication(ctx, client, inst, sts, logger) + if err != nil { logger.Error(err, "parse shard failed") - } else { - shards = append(shards, node) } - return shards, nil -} - -func (s *RedisSentinelReplica) Version() redis.RedisVersion { - if s == nil { - return redis.RedisVersionUnknown - } - - container := util.GetContainerByName(&s.Spec.Template.Spec, sentinelbuilder.ServerContainerName) - ver, _ := redis.ParseRedisVersionFromImage(container.Image) - return ver + return repl, nil } -func NewRedisSentinelReplica(ctx context.Context, client clientset.ClientSet, sentinel types.RedisInstance, sts *appv1.StatefulSet, logger logr.Logger) (types.RedisSentinelReplica, error) { +func NewRedisReplication(ctx context.Context, client clientset.ClientSet, inst types.RedisFailoverInstance, sts *appv1.StatefulSet, logger logr.Logger) (types.RedisReplication, error) { if client == nil { return nil, fmt.Errorf("require clientset") } - if sentinel == nil { - return nil, fmt.Errorf("require cluster instance") + if inst == nil { + return nil, fmt.Errorf("require instance") } if sts == nil { return nil, fmt.Errorf("require statefulset") } - sentinelNode := &RedisSentinelReplica{ + repl := &RedisReplication{ StatefulSet: *sts, client: client, - sentinel: sentinel, + failover: inst, logger: logger, } - users := sentinel.Users() + users := inst.Users() var err error - if sentinelNode.nodes, err = models.LoadRedisNodes(ctx, client, sts, users.GetOpUser(), logger); err != nil { + if repl.nodes, err = model.LoadRedisNodes(ctx, client, sts, users.GetOpUser(), logger); err != nil { logger.Error(err, "load shard nodes failed", "shard", sts.GetName()) return nil, err } - return sentinelNode, nil + return repl, nil } -func (s *RedisSentinelReplica) Nodes() []redis.RedisNode { +func (s *RedisReplication) NamespacedName() client.ObjectKey { + if s == nil { + return k8stypes.NamespacedName{} + } + return client.ObjectKey{ + Namespace: s.Namespace, + Name: s.Name, + } +} + +func (s *RedisReplication) Version() redis.RedisVersion { + if s == nil { + return redis.RedisVersionUnknown + } + + container := util.GetContainerByName(&s.Spec.Template.Spec, failoverbuilder.ServerContainerName) + ver, _ := redis.ParseRedisVersionFromImage(container.Image) + return ver +} + +func (s *RedisReplication) Nodes() []redis.RedisNode { if s == nil { return nil } return s.nodes } -func (s *RedisSentinelReplica) Replicas() []redis.RedisNode { +func (s *RedisReplication) Replicas() []redis.RedisNode { if s == nil || len(s.nodes) == 0 { return nil } var replicas []redis.RedisNode for _, node := range s.nodes { - if node.Role() == redis.RedisRoleSlave { + if node.Role() == core.RedisRoleReplica { replicas = append(replicas, node) } } return replicas } -func (s *RedisSentinelReplica) Master() redis.RedisNode { +func (s *RedisReplication) Master() redis.RedisNode { if s == nil || len(s.nodes) == 0 { return nil } @@ -132,7 +139,7 @@ func (s *RedisSentinelReplica) Master() redis.RedisNode { var master redis.RedisNode for _, node := range s.nodes { // if the node joined, and is master, then it's the master - if node.Role() == redis.RedisRoleMaster { + if node.Role() == core.RedisRoleMaster { master = node } } @@ -140,17 +147,31 @@ func (s *RedisSentinelReplica) Master() redis.RedisNode { return master } -func (s *RedisSentinelReplica) Restart(ctx context.Context) error { +func (s *RedisReplication) IsReady() bool { + if s == nil { + return false + } + return s.Status().ReadyReplicas == *s.Spec.Replicas && s.Status().UpdateRevision == s.Status().CurrentRevision +} + +func (s *RedisReplication) Restart(ctx context.Context, annotationKeyVal ...string) error { + if s == nil { + return nil + } // update all shards logger := s.logger.WithName("Restart") + kv := map[string]string{ + "kubectl.kubernetes.io/restartedAt": time.Now().Format(time.RFC3339Nano), + } + for i := 0; i < len(annotationKeyVal)-1; i += 2 { + kv[annotationKeyVal[i]] = annotationKeyVal[i+1] + } data, _ := json.Marshal(map[string]interface{}{ "spec": map[string]interface{}{ "template": map[string]interface{}{ "metadata": map[string]interface{}{ - "annotations": map[string]string{ - "kubectl.kubernetes.io/restartedAt": time.Now().Format(time.RFC3339Nano), - }, + "annotations": kv, }, }, }, @@ -164,25 +185,28 @@ func (s *RedisSentinelReplica) Restart(ctx context.Context) error { return nil } -func (s *RedisSentinelReplica) Refresh(ctx context.Context) error { +func (s *RedisReplication) Refresh(ctx context.Context) error { + if s == nil { + return nil + } logger := s.logger.WithName("Refresh") var err error - if s.nodes, err = models.LoadRedisNodes(ctx, s.client, &s.StatefulSet, s.sentinel.Users().GetOpUser(), logger); err != nil { + if s.nodes, err = model.LoadRedisNodes(ctx, s.client, &s.StatefulSet, s.failover.Users().GetOpUser(), logger); err != nil { logger.Error(err, "load shard nodes failed", "shard", s.GetName()) return err } return nil } -func (s *RedisSentinelReplica) Status() *appv1.StatefulSetStatus { +func (s *RedisReplication) Status() *appv1.StatefulSetStatus { if s == nil { return nil } return &s.StatefulSet.Status } -func (s *RedisSentinelReplica) Definition() *appv1.StatefulSet { +func (s *RedisReplication) Definition() *appv1.StatefulSet { if s == nil { return nil } diff --git a/pkg/models/node.go b/internal/redis/redis.go similarity index 68% rename from pkg/models/node.go rename to internal/redis/redis.go index e7c9075..453f1e1 100644 --- a/pkg/models/node.go +++ b/internal/redis/redis.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package models +package redis import ( "context" @@ -28,15 +28,15 @@ import ( "strings" "time" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/util" "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" rediscli "github.com/alauda/redis-operator/pkg/redis" - "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/slot" "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" "github.com/go-logr/logr" appv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -44,71 +44,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -var _ types.RedisNode = (*RedisNode)(nil) - -func LoadRedisSentinelNodes(ctx context.Context, client kubernetes.ClientSet, deploy *appv1.Deployment, newUser *user.User, logger logr.Logger) ([]redis.RedisNode, error) { - if client == nil { - return nil, fmt.Errorf("require clientset") - } - if deploy == nil { - return nil, fmt.Errorf("require deployment") - } - pods, err := client.GetDeploymentPods(ctx, deploy.GetNamespace(), deploy.GetName()) - if err != nil { - logger.Error(err, "loads pods of shard failed") - return nil, err - } - nodes := []redis.RedisNode{} - for _, pod := range pods.Items { - if node, err := NewRedisSentinelNode(ctx, client, deploy, &pod, newUser, logger); err != nil { - logger.Error(err, "parse redis node failed", "pod", pod.Name) - } else { - nodes = append(nodes, node) - } - } - return nodes, nil -} - -func NewRedisSentinelNode(ctx context.Context, client kubernetes.ClientSet, deploy *appv1.Deployment, pod *corev1.Pod, newUser *user.User, logger logr.Logger) (redis.RedisNode, error) { - if client == nil { - return nil, fmt.Errorf("require clientset") - } - if deploy == nil { - return nil, fmt.Errorf("require deployment") - } - if pod == nil { - return nil, fmt.Errorf("require pod") - } - - node := RedisNode{ - Pod: *pod, - client: client, - deployment: deploy, - newUser: newUser, - logger: logger.WithName("RedisNode"), - } - - var err error - if node.opUser, err = node.loadOperatorUser(ctx); err != nil { - return nil, err - } - if node.tlsConfig, err = node.loadTLS(ctx); err != nil { - return nil, err - } - - if node.IsContainerReady() { - redisCli, err := node.getRedisConnect(ctx, &node) - if err != nil { - return nil, err - } - defer redisCli.Close() - - if node.info, node.config, node.nodes, err = node.loadRedisInfo(ctx, &node, redisCli); err != nil { - return nil, err - } - } - return &node, nil -} +var _ redis.RedisNode = (*RedisNode)(nil) // LoadRedisNodes func LoadRedisNodes(ctx context.Context, client kubernetes.ClientSet, sts *appv1.StatefulSet, newUser *user.User, logger logr.Logger) ([]redis.RedisNode, error) { @@ -128,6 +64,7 @@ func LoadRedisNodes(ctx context.Context, client kubernetes.ClientSet, sts *appv1 nodes := []redis.RedisNode{} for _, pod := range pods.Items { + pod := pod.DeepCopy() if !func() bool { for _, own := range pod.OwnerReferences { if own.UID == sts.GetUID() { @@ -139,7 +76,7 @@ func LoadRedisNodes(ctx context.Context, client kubernetes.ClientSet, sts *appv1 continue } - if node, err := NewRedisNode(ctx, client, sts, &pod, newUser, logger); err != nil { + if node, err := NewRedisNode(ctx, client, sts, pod, newUser, logger); err != nil { logger.Error(err, "parse redis node failed", "pod", pod.Name) } else { nodes = append(nodes, node) @@ -151,58 +88,22 @@ func LoadRedisNodes(ctx context.Context, client kubernetes.ClientSet, sts *appv1 return nodes, nil } -func LoadSentinelRedisNodes(ctx context.Context, client kubernetes.ClientSet, sts *appv1.StatefulSet, newUser *user.User, logger logr.Logger) ([]redis.RedisNode, error) { - if client == nil { - return nil, fmt.Errorf("require clientset") - } - if sts == nil { - return nil, fmt.Errorf("require statefulset") - } - - // load pods by statefulset selector - pods, err := client.GetStatefulSetPodsByLabels(ctx, sts.GetNamespace(), sts.Spec.Selector.MatchLabels) - if err != nil { - logger.Error(err, "loads pods of shard failed") - return nil, err - } - - nodes := []redis.RedisNode{} - for _, pod := range pods.Items { - if !func() bool { - for _, own := range pod.OwnerReferences { - if own.UID == sts.GetUID() { - return true - } - } - return false - }() { - continue - } - - if node, err := NewRedisNode(ctx, client, sts, &pod, newUser, logger); err != nil { - logger.Error(err, "parse redis node failed", "pod", pod.Name) - } else { - nodes = append(nodes, node) - } - } - return nodes, nil -} - type RedisNode struct { corev1.Pod + client kubernetes.ClientSet statefulSet *appv1.StatefulSet - deployment *appv1.Deployment - opUser *user.User + localUser *user.User newUser *user.User tlsConfig *tls.Config info *rediscli.RedisInfo + cinfo *rediscli.RedisClusterInfo nodes rediscli.ClusterNodes config map[string]string - logger logr.Logger -} -type RedisSentinelNode struct { + // TODO: added a flag to indicate redis-server is not connectable + + logger logr.Logger } func (n *RedisNode) Definition() *corev1.Pod { @@ -233,7 +134,7 @@ func NewRedisNode(ctx context.Context, client kubernetes.ClientSet, sts *appv1.S } var err error - if node.opUser, err = node.loadOperatorUser(ctx); err != nil { + if node.localUser, err = node.loadLocalUser(ctx); err != nil { return nil, err } if node.tlsConfig, err = node.loadTLS(ctx); err != nil { @@ -247,14 +148,16 @@ func NewRedisNode(ctx context.Context, client kubernetes.ClientSet, sts *appv1.S } defer redisCli.Close() - if node.info, node.config, node.nodes, err = node.loadRedisInfo(ctx, &node, redisCli); err != nil { + // TODO: list the pod status, but added a flag to indicate redis-server is not connectable, + // maybe redis-server blocked or the host node is down + if node.info, node.cinfo, node.config, node.nodes, err = node.loadRedisInfo(ctx, &node, redisCli); err != nil { return nil, err } } return &node, nil } -// loadOperatorUser +// loadLocalUser // // every pod still mount secret to the pod. for acl supported and not supported versions, the difference is that: // unsupported: the mount secret is the default user secret, which maybe changed @@ -262,19 +165,16 @@ func NewRedisNode(ctx context.Context, client kubernetes.ClientSet, sts *appv1.S // // this method is used to fetch pod's operator secret // for versions without acl supported, there exists cases that the env secret not consistent with the server -func (s *RedisNode) loadOperatorUser(ctx context.Context) (*user.User, error) { +func (s *RedisNode) loadLocalUser(ctx context.Context) (*user.User, error) { if s == nil { return nil, nil } - logger := s.logger.WithName("loadOperatorUser") + logger := s.logger.WithName("loadLocalUser") var ( secretName string username string ) - if s.IsSentinelPod() { - return user.NewUser("", user.RoleDeveloper, nil) - } container := util.GetContainerByName(&s.Spec, clusterbuilder.ServerContainerName) if container == nil { return nil, fmt.Errorf("server container not found") @@ -288,19 +188,28 @@ func (s *RedisNode) loadOperatorUser(ctx context.Context) (*user.User, error) { username = env.Value } } + if secretName == "" { + // COMPAT: for old sentinel version, the secret is mounted to the pod + for _, vol := range s.Spec.Volumes { + if vol.Name == "redis-auth" && vol.Secret != nil { + secretName = vol.Secret.SecretName + break + } + } + } if secretName != "" { if secret, err := s.client.GetSecret(ctx, s.GetNamespace(), secretName); err != nil { - logger.Error(err, "get user secret failed", "target", fmt.Sprintf("%s/%s", s.GetNamespace(), secretName)) + logger.Error(err, "get user secret failed", "target", util.ObjectKey(s.GetNamespace(), secretName)) return nil, err - } else if user, err := user.NewUser(username, user.RoleDeveloper, secret); err != nil { + } else if user, err := user.NewUser(username, user.RoleDeveloper, secret, s.CurrentVersion().IsACL2Supported()); err != nil { return nil, err } else { return user, nil } } // return default user with out password - return user.NewUser("", user.RoleDeveloper, nil) + return user.NewUser("", user.RoleDeveloper, nil, s.CurrentVersion().IsACL2Supported()) } func (n *RedisNode) loadTLS(ctx context.Context) (*tls.Config, error) { @@ -343,7 +252,7 @@ func (n *RedisNode) loadTLS(ctx context.Context) (*tls.Config, error) { caCertPool.AppendCertsFromPEM(secret.Data["ca.crt"]) return &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, // #nosec RootCAs: caCertPool, Certificates: []tls.Certificate{cert}, }, nil @@ -355,26 +264,15 @@ func (n *RedisNode) getRedisConnect(ctx context.Context, node *RedisNode) (redis } logger := n.logger.WithName("getRedisConnect") - if n.Status() != corev1.PodRunning || !n.IsContainerReady() { + if !n.IsContainerReady() { logger.Error(fmt.Errorf("get redis info failed"), "pod not ready", "target", client.ObjectKey{Namespace: node.Namespace, Name: node.Name}) return nil, fmt.Errorf("node not ready") } - var ( - err error - host string - port = n.InternalPort() - ) - // this should not happen for running pods - if host = node.DefaultInternalIP().String(); host == "" { - return nil, fmt.Errorf("no ip found for pod %s", node.Name) - } - if strings.Contains(host, ":") { - host = fmt.Sprintf("[%s]", host) - } - - for _, user := range []*user.User{node.opUser, node.newUser} { + var err error + addr := net.JoinHostPort(node.DefaultInternalIP().String(), strconv.Itoa(n.InternalPort())) + for _, user := range []*user.User{node.newUser, node.localUser} { if user == nil { continue } @@ -383,10 +281,7 @@ func (n *RedisNode) getRedisConnect(ctx context.Context, node *RedisNode) (redis name = "" } password := user.Password.String() - if node.IsSentinelPod() { - password = "" - } - rediscli := rediscli.NewRedisClient(fmt.Sprintf("%s:%d", host, port), rediscli.AuthConfig{ + rediscli := rediscli.NewRedisClient(addr, rediscli.AuthConfig{ Username: name, Password: password, TLSConfig: node.tlsConfig, @@ -415,24 +310,27 @@ func (n *RedisNode) getRedisConnect(ctx context.Context, node *RedisNode) (redis // loadRedisInfo func (n *RedisNode) loadRedisInfo(ctx context.Context, node *RedisNode, redisCli rediscli.RedisClient) (info *rediscli.RedisInfo, - config map[string]string, nodes rediscli.ClusterNodes, err error) { + cinfo *rediscli.RedisClusterInfo, config map[string]string, nodes rediscli.ClusterNodes, err error) { // fetch redis info if info, err = redisCli.Info(ctx); err != nil { n.logger.Error(err, "load redis info failed") - return nil, nil, nil, err + return nil, nil, nil, nil, err } - if !n.IsSentinelPod() { - // fetch current config - if config, err = redisCli.ConfigGet(ctx, "*"); err != nil { - n.logger.Error(err, "get redis config failed, ignore") - } + // fetch current config + if config, err = redisCli.ConfigGet(ctx, "*"); err != nil { + n.logger.Error(err, "get redis config failed, ignore") } if info.ClusterEnabled == "1" { + if cinfo, err = redisCli.ClusterInfo(ctx); err != nil { + n.logger.Error(err, "load redis cluster info failed") + } + // load cluster nodes if items, err := redisCli.Nodes(ctx); err != nil { n.logger.Error(err, "load redis cluster nodes info failed, ignore") } else { + // TODO: port this logic to Clean actor for _, n := range items { // clean disconnected nodes if n.LinkState == "disconnected" && strings.Contains(n.RawFlag, "noaddr") { @@ -444,9 +342,9 @@ func (n *RedisNode) loadRedisInfo(ctx context.Context, node *RedisNode, redisCli } } } - } - return info, config, nodes, nil + + return } // Refresh not concurrency safe @@ -463,11 +361,11 @@ func (n *RedisNode) Refresh(ctx context.Context) (err error) { n.Pod = *pod } - opUser, err := n.loadOperatorUser(ctx) + localUser, err := n.loadLocalUser(ctx) if err != nil { return err } else { - n.opUser = opUser + n.localUser = localUser } if n.IsContainerReady() { @@ -477,7 +375,7 @@ func (n *RedisNode) Refresh(ctx context.Context) (err error) { } defer redisCli.Close() - if n.info, n.config, n.nodes, err = n.loadRedisInfo(ctx, n, redisCli); err != nil { + if n.info, n.cinfo, n.config, n.nodes, err = n.loadRedisInfo(ctx, n, redisCli); err != nil { n.logger.Error(err, "refresh info failed") return err } @@ -517,7 +415,7 @@ func (n *RedisNode) IsMasterFailed() bool { if self == nil { return false } - if n.Role() == redis.RedisRoleMaster { + if n.Role() == core.RedisRoleMaster { return false } if self.MasterId != "" { @@ -548,7 +446,7 @@ func (n *RedisNode) IsContainerReady() bool { } for _, cond := range n.Pod.Status.ContainerStatuses { - if cond.Name == clusterbuilder.ServerContainerName || cond.Name == sentinelbuilder.SentinelContainerName { + if cond.Name == clusterbuilder.ServerContainerName { // assume the main process is ready in 10s if cond.Started != nil && *cond.Started && cond.State.Running != nil && time.Since(cond.State.Running.StartedAt.Time) > time.Second*10 { @@ -559,18 +457,6 @@ func (n *RedisNode) IsContainerReady() bool { return false } -func (n *RedisNode) IsSentinelPod() bool { - if n == nil { - return false - } - for _, cond := range n.Pod.Status.ContainerStatuses { - if cond.Name == sentinelbuilder.SentinelContainerName { - return true - } - } - return false -} - // IsReady func (n *RedisNode) IsReady() bool { if n == nil { @@ -599,6 +485,10 @@ func (n *RedisNode) IsMasterLinkUp() bool { if n == nil || n.info == nil { return false } + + if n.Role() == core.RedisRoleMaster { + return true + } return n.info.MasterLinkStatus == "up" } @@ -623,7 +513,7 @@ func (n *RedisNode) Slots() *slot.Slots { } role := n.Role() - if self := n.nodes.Self(); self != nil && role == redis.RedisRoleMaster { + if self := n.nodes.Self(); self != nil && role == core.RedisRoleMaster { slots := slot.NewSlots() if err := slots.Load(self.Slots); err != nil { // this should not happen @@ -652,9 +542,6 @@ func (n *RedisNode) Index() int { func (n *RedisNode) IsACLApplied() bool { // check if acl have been applied to container container := util.GetContainerByName(&n.Pod.Spec, clusterbuilder.ServerContainerName) - if n.IsSentinelPod() { - container = util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName) - } for _, env := range container.Env { if env.Name == "ACL_CONFIGMAP_NAME" { return true @@ -670,9 +557,6 @@ func (n *RedisNode) CurrentVersion() redis.RedisVersion { // parse version from redis image container := util.GetContainerByName(&n.Pod.Spec, clusterbuilder.ServerContainerName) - if n.IsSentinelPod() { - container = util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName) - } if ver, _ := redis.ParseRedisVersionFromImage(container.Image); ver != redis.RedisVersionUnknown { return ver } @@ -681,9 +565,9 @@ func (n *RedisNode) CurrentVersion() redis.RedisVersion { return v } -func (n *RedisNode) Role() redis.RedisRole { +func (n *RedisNode) Role() core.RedisRole { if n == nil || n.info == nil { - return redis.RedisRoleNone + return core.RedisRoleNone } return redis.NewRedisRole(n.info.Role) } @@ -721,6 +605,7 @@ func (n *RedisNode) Setup(ctx context.Context, margs ...[]any) (err error) { } defer redisCli.Close() + // TODO: change this to pipeline for _, args := range margs { if len(args) == 0 { continue @@ -753,6 +638,12 @@ func (n *RedisNode) SetACLUser(ctx context.Context, username string, passwords [ return nil, err } defer redisCli.Close() + if acluser, err := rediscli.String(redisCli.Do(ctx, "ACL", "whoami")); err != nil { + return nil, err + } else if acluser != user.DefaultOperatorUserName { + return nil, fmt.Errorf("user not operator") + } + cmds := [][]interface{}{{"ACL", "SETUSER", username, "reset"}} for _, password := range passwords { cmds = append(cmds, []interface{}{"ACL", "SETUSER", username, ">" + password}) @@ -779,7 +670,7 @@ func (n *RedisNode) SetACLUser(ctx context.Context, username string, passwords [ } ctx, cancel := context.WithTimeout(ctx, time.Second*3) defer cancel() - results, err := redisCli.Pipelining(ctx, cmd_list, args_list) + results, err := redisCli.Tx(ctx, cmd_list, args_list) if err != nil { return nil, err } @@ -829,35 +720,20 @@ func (n *RedisNode) Info() rediscli.RedisInfo { return *n.info } -func (n *RedisNode) Port() int { - port := 6379 - if container := util.GetContainerByName(&n.Pod.Spec, clusterbuilder.ServerContainerName); container != nil { - for _, p := range container.Ports { - if p.Name == clusterbuilder.RedisDataContainerPortName { - port = int(p.ContainerPort) - break - } - } +func (n *RedisNode) ClusterInfo() rediscli.RedisClusterInfo { + if n == nil || n.cinfo == nil { + return rediscli.RedisClusterInfo{} } - if n.IsSentinelPod() { - if container := util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName); container != nil { - for _, p := range container.Ports { - if p.Name == sentinelbuilder.SentinelContainerPortName { - port = int(p.ContainerPort) - break - } - } - } - } - if value, ok := n.Pod.Labels["middleware.alauda.io/announce_port"]; ok { - if value != "" { - _port, err := strconv.Atoi(value) - if err == nil { - port = _port - } + return *n.cinfo +} + +func (n *RedisNode) Port() int { + if value := n.Pod.Labels[builder.PodAnnouncePortLabelKey]; value != "" { + if port, _ := strconv.Atoi(value); port > 0 { + return port } } - return port + return n.InternalPort() } func (n *RedisNode) InternalPort() int { @@ -870,28 +746,15 @@ func (n *RedisNode) InternalPort() int { } } } - if container := util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName); container != nil { - for _, p := range container.Ports { - if p.Name == sentinelbuilder.SentinelContainerPortName { - port = int(p.ContainerPort) - break - } - } - } - return port } func (n *RedisNode) DefaultIP() net.IP { - if value := n.Pod.Labels["middleware.alauda.io/announce_ip"]; value != "" { + if value := n.Pod.Labels[builder.PodAnnounceIPLabelKey]; value != "" { address := strings.Replace(value, "-", ":", -1) return net.ParseIP(address) } - ips := n.IPs() - if len(ips) > 0 { - return ips[0] - } - return nil + return n.DefaultInternalIP() } func (n *RedisNode) DefaultInternalIP() net.IP { @@ -909,14 +772,6 @@ func (n *RedisNode) DefaultInternalIP() net.IP { } } } - if container := util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName); container != nil { - for _, env := range container.Env { - if env.Name == "IP_FAMILY_PREFER" { - ipFamilyPrefer = env.Value - break - } - } - } if ipFamilyPrefer != "" { for _, ip := range n.IPs() { @@ -934,7 +789,7 @@ func (n *RedisNode) DefaultInternalIP() net.IP { } func (n *RedisNode) IPort() int { - if value := n.Pod.Labels["middleware.alauda.io/announce_iport"]; value != "" { + if value := n.Pod.Labels[builder.PodAnnounceIPortLabelKey]; value != "" { port, err := strconv.Atoi(value) if err == nil { return port @@ -990,36 +845,6 @@ func (n *RedisNode) Status() corev1.PodPhase { return n.Pod.Status.Phase } -func (n *RedisNode) SetMonitor(ctx context.Context, ip, port, user, password, quorum string) error { - - if err := n.Setup(ctx, []interface{}{"sentinel", "remove", "mymaster"}); err != nil { - n.logger.Info("try remove mymaster failed", "err", err.Error()) - } - - n.logger.Info("set monitor", "ip", ip, "port", port, "quorum", quorum) - if err := n.Setup(ctx, []interface{}{"sentinel", "monitor", "mymaster", ip, port, quorum}); err != nil { - return err - } - if password != "" { - if n.CurrentVersion().IsACLSupported() { - if user != "" { - if err := n.Setup(ctx, []interface{}{"sentinel", "set", "mymaster", "auth-user", user}); err != nil { - return err - } - } - } - if err := n.Setup(ctx, []interface{}{"sentinel", "set", "mymaster", "auth-pass", password}); err != nil { - return err - } - //reset - if err := n.Setup(ctx, []interface{}{"sentinel", "reset", "mymaster"}); err != nil { - return err - } - } - - return nil -} - func (n *RedisNode) ReplicaOf(ctx context.Context, ip, port string) error { if n.DefaultIP().String() == ip && strconv.Itoa(n.Port()) == port { return nil diff --git a/internal/redis/sentinel/node.go b/internal/redis/sentinel/node.go new file mode 100644 index 0000000..4cc29fc --- /dev/null +++ b/internal/redis/sentinel/node.go @@ -0,0 +1,672 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinel + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/netip" + "strconv" + "strings" + "time" + + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/clusterbuilder" + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + "github.com/alauda/redis-operator/internal/util" + "github.com/alauda/redis-operator/pkg/kubernetes" + rediscli "github.com/alauda/redis-operator/pkg/redis" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/pkg/types/user" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func LoadRedisSentinelNodes(ctx context.Context, client kubernetes.ClientSet, sts metav1.Object, newUser *user.User, logger logr.Logger) ([]redis.RedisSentinelNode, error) { + if client == nil { + return nil, fmt.Errorf("require clientset") + } + if sts == nil { + return nil, fmt.Errorf("require statefulset") + } + pods, err := client.GetStatefulSetPods(ctx, sts.GetNamespace(), sts.GetName()) + if err != nil { + logger.Error(err, "loads pods of shard failed") + if errors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + nodes := []redis.RedisSentinelNode{} + for _, pod := range pods.Items { + pod := pod.DeepCopy() + if node, err := NewRedisSentinelNode(ctx, client, sts, pod, newUser, logger); err != nil { + logger.Error(err, "parse redis node failed", "pod", pod.Name) + } else { + nodes = append(nodes, node) + } + } + return nodes, nil +} + +func LoadDeploymentRedisSentinelNodes(ctx context.Context, client kubernetes.ClientSet, obj metav1.Object, newUser *user.User, logger logr.Logger) ([]redis.RedisSentinelNode, error) { + if client == nil { + return nil, fmt.Errorf("require clientset") + } + if obj == nil { + return nil, fmt.Errorf("require statefulset") + } + pods, err := client.GetDeploymentPods(ctx, obj.GetNamespace(), obj.GetName()) + if err != nil { + if errors.IsNotFound(err) { + return nil, nil + } + logger.Error(err, "loads pods of shard failed") + return nil, err + } + nodes := []redis.RedisSentinelNode{} + for _, pod := range pods.Items { + pod := pod.DeepCopy() + if node, err := NewRedisSentinelNode(ctx, client, obj, pod, newUser, logger); err != nil { + logger.Error(err, "parse redis node failed", "pod", pod.Name) + } else { + nodes = append(nodes, node) + } + } + return nodes, nil +} + +func NewRedisSentinelNode(ctx context.Context, client kubernetes.ClientSet, obj metav1.Object, pod *corev1.Pod, newUser *user.User, logger logr.Logger) (redis.RedisSentinelNode, error) { + if client == nil { + return nil, fmt.Errorf("require clientset") + } + if obj == nil { + return nil, fmt.Errorf("require workload object") + } + if pod == nil { + return nil, fmt.Errorf("require pod") + } + + node := RedisSentinelNode{ + Pod: *pod, + parent: obj, + client: client, + newUser: newUser, + logger: logger.WithName("RedisSentinelNode"), + } + + var err error + if node.localUser, err = node.loadLocalUser(ctx); err != nil { + return nil, err + } + if node.tlsConfig, err = node.loadTLS(ctx); err != nil { + return nil, err + } + + if node.IsContainerReady() { + redisCli, err := node.getRedisConnect(ctx, &node) + if err != nil { + return nil, err + } + defer redisCli.Close() + + if node.info, node.config, err = node.loadRedisInfo(ctx, &node, redisCli); err != nil { + return nil, err + } + } + return &node, nil +} + +var _ redis.RedisSentinelNode = &RedisSentinelNode{} + +type RedisSentinelNode struct { + corev1.Pod + parent metav1.Object + client kubernetes.ClientSet + localUser *user.User + newUser *user.User + tlsConfig *tls.Config + info *rediscli.RedisInfo + config map[string]string + logger logr.Logger +} + +func (n *RedisSentinelNode) Definition() *corev1.Pod { + if n == nil { + return nil + } + return &n.Pod +} + +// loadLocalUser +// +// every pod still mount secret to the pod. for acl supported and not supported versions, the difference is that: +// unsupported: the mount secret is the default user secret, which maybe changed +// supported: the mount secret is operator's secret. the operator secret never changes, which is used only internal +// +// this method is used to fetch pod's operator secret +// for versions without acl supported, there exists cases that the env secret not consistent with the server +func (s *RedisSentinelNode) loadLocalUser(ctx context.Context) (*user.User, error) { + if s == nil { + return nil, nil + } + logger := s.logger.WithName("loadLocalUser") + + var secretName string + container := util.GetContainerByName(&s.Spec, sentinelbuilder.SentinelContainerName) + if container == nil { + return nil, fmt.Errorf("server container not found") + } + for _, env := range container.Env { + if env.Name == sentinelbuilder.OperatorSecretName && env.Value != "" { + secretName = env.Value + break + } + } + if secretName != "" { + if secret, err := s.client.GetSecret(ctx, s.GetNamespace(), secretName); err != nil { + logger.Error(err, "get user secret failed", "target", util.ObjectKey(s.GetNamespace(), secretName)) + return nil, err + } else if user, err := user.NewSentinelUser("", user.RoleDeveloper, secret); err != nil { + return nil, err + } else { + return user, nil + } + } + // return default user with out password + return user.NewSentinelUser("", user.RoleDeveloper, nil) +} + +func (n *RedisSentinelNode) loadTLS(ctx context.Context) (*tls.Config, error) { + if n == nil { + return nil, nil + } + logger := n.logger + + var name string + for _, vol := range n.Spec.Volumes { + if vol.Name == clusterbuilder.RedisTLSVolumeName && vol.Secret != nil && vol.Secret.SecretName != "" { + name = vol.Secret.SecretName + break + } + } + if name == "" { + return nil, nil + } + + secret, err := n.client.GetSecret(ctx, n.GetNamespace(), name) + if err != nil { + if errors.IsNotFound(err) { + return nil, nil + } + logger.Error(err, "get secret failed", "target", name) + return nil, err + } + + if secret.Data[corev1.TLSCertKey] == nil || secret.Data[corev1.TLSPrivateKeyKey] == nil || + secret.Data["ca.crt"] == nil { + logger.Error(fmt.Errorf("invalid tls secret"), "tls secret is invaid") + return nil, fmt.Errorf("tls secret is invalid") + } + cert, err := tls.X509KeyPair(secret.Data[corev1.TLSCertKey], secret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + logger.Error(err, "generate X509KeyPair failed") + return nil, err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(secret.Data["ca.crt"]) + + return &tls.Config{ + InsecureSkipVerify: true, // #nosec + RootCAs: caCertPool, + Certificates: []tls.Certificate{cert}, + }, nil +} + +func (n *RedisSentinelNode) getRedisConnect(ctx context.Context, node *RedisSentinelNode) (rediscli.RedisClient, error) { + if n == nil { + return nil, fmt.Errorf("nil node") + } + logger := n.logger.WithName("getRedisConnect") + + if !n.IsContainerReady() { + logger.Error(fmt.Errorf("get redis info failed"), "pod not ready", "target", + client.ObjectKey{Namespace: node.Namespace, Name: node.Name}) + return nil, fmt.Errorf("node not ready") + } + + var ( + err error + addr = net.JoinHostPort(node.DefaultInternalIP().String(), strconv.Itoa(n.InternalPort())) + ) + for _, user := range []*user.User{node.newUser, node.localUser} { + if user == nil { + continue + } + rediscli := rediscli.NewRedisClient(addr, rediscli.AuthConfig{ + Username: user.Name, + Password: user.Password.String(), + TLSConfig: node.tlsConfig, + }) + + nctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + if err = rediscli.Ping(nctx); err != nil { + if strings.Contains(err.Error(), "NOAUTH Authentication required") || + strings.Contains(err.Error(), "invalid password") || + strings.Contains(err.Error(), "Client sent AUTH, but no password is set") || + strings.Contains(err.Error(), "invalid username-password pair") { + continue + } + logger.Error(err, "check connection to redis failed", "address", addr) + return nil, err + } + return rediscli, nil + } + if err == nil { + err = fmt.Errorf("no usable account to connect to redis instance") + } + return nil, err +} + +// loadRedisInfo +func (n *RedisSentinelNode) loadRedisInfo(ctx context.Context, _ *RedisSentinelNode, redisCli rediscli.RedisClient) (info *rediscli.RedisInfo, + config map[string]string, err error) { + // fetch redis info + if info, err = redisCli.Info(ctx); err != nil { + n.logger.Error(err, "load redis info failed") + return nil, nil, err + } + return +} + +// Index returns the index of the related pod +func (n *RedisSentinelNode) Index() int { + if n == nil { + return -1 + } + + name := n.Pod.Name + if i := strings.LastIndex(name, "-"); i > 0 { + index, _ := strconv.ParseInt(name[i+1:], 10, 64) + return int(index) + } + return -1 +} + +// Refresh not concurrency safe +func (n *RedisSentinelNode) Refresh(ctx context.Context) (err error) { + if n == nil { + return nil + } + + // refresh pod first + if pod, err := n.client.GetPod(ctx, n.GetNamespace(), n.GetName()); err != nil { + n.logger.Error(err, "refresh pod failed") + return err + } else { + n.Pod = *pod + } + + if n.IsContainerReady() { + redisCli, err := n.getRedisConnect(ctx, n) + if err != nil { + return err + } + defer redisCli.Close() + + if n.info, n.config, err = n.loadRedisInfo(ctx, n, redisCli); err != nil { + n.logger.Error(err, "refresh info failed") + return err + } + } + return nil +} + +// IsContainerReady +func (n *RedisSentinelNode) IsContainerReady() bool { + if n == nil { + return false + } + + for _, cond := range n.Pod.Status.ContainerStatuses { + if cond.Name == sentinelbuilder.SentinelContainerName { + // assume the main process is ready in 10s + if cond.Started != nil && *cond.Started && cond.State.Running != nil && + time.Since(cond.State.Running.StartedAt.Time) > time.Second*10 { + return true + } + } + } + return false +} + +// IsReady +func (n *RedisSentinelNode) IsReady() bool { + if n == nil || n.IsTerminating() { + return false + } + + for _, cond := range n.Pod.Status.ContainerStatuses { + if cond.Name == sentinelbuilder.SentinelContainerName { + return cond.Ready + } + } + return false +} + +// IsTerminating +func (n *RedisSentinelNode) IsTerminating() bool { + if n == nil { + return false + } + + return n.DeletionTimestamp != nil +} + +func (n *RedisSentinelNode) IsACLApplied() bool { + // check if acl have been applied to container + container := util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName) + for _, env := range container.Env { + if env.Name == "ACL_CONFIGMAP_NAME" { + return true + } + } + return false +} + +func (n *RedisSentinelNode) CurrentVersion() redis.RedisVersion { + if n == nil { + return "" + } + + // parse version from redis image + container := util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName) + if ver, _ := redis.ParseRedisVersionFromImage(container.Image); ver != redis.RedisVersionUnknown { + return ver + } + v, _ := redis.ParseRedisVersion(n.info.RedisVersion) + return v +} + +func (n *RedisSentinelNode) Config() map[string]string { + if n == nil || n.config == nil { + return nil + } + return n.config +} + +// Setup only return the last command error +func (n *RedisSentinelNode) Setup(ctx context.Context, margs ...[]any) (err error) { + if n == nil { + return nil + } + + redisCli, err := n.getRedisConnect(ctx, n) + if err != nil { + return err + } + defer redisCli.Close() + + // TODO: change this to pipeline + for _, args := range margs { + if len(args) == 0 { + continue + } + cmd, ok := args[0].(string) + if !ok { + return fmt.Errorf("the command must be string") + } + + func() { + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + if _, err = redisCli.Do(ctx, cmd, args[1:]...); err != nil { + // ignore forget nodes error + n.logger.Error(err, "set config failed", "target", n.GetName(), "address", n.DefaultInternalIP().String(), "port", n.InternalPort(), "cmd", cmd) + } + }() + } + return +} + +func (n *RedisSentinelNode) Query(ctx context.Context, cmd string, args ...any) (any, error) { + if n == nil { + return nil, nil + } + + redisCli, err := n.getRedisConnect(ctx, n) + if err != nil { + return nil, err + } + defer redisCli.Close() + + ctx, cancel := context.WithTimeout(ctx, time.Second*3) + defer cancel() + + return redisCli.Do(ctx, cmd, args...) +} + +func (n *RedisSentinelNode) Brothers(ctx context.Context, name string) (ret []*rediscli.SentinelMonitorNode, err error) { + if n == nil { + return nil, nil + } + + items, err := rediscli.Values(n.Query(ctx, "SENTINEL", "SENTINELS", name)) + if err != nil { + if strings.Contains(err.Error(), "No such master with that name") { + return nil, nil + } + return nil, err + } + for _, item := range items { + node := rediscli.ParseSentinelMonitorNode(item) + ret = append(ret, node) + } + return +} + +func (n *RedisSentinelNode) MonitoringClusters(ctx context.Context) (clusters []string, err error) { + if n == nil { + return nil, nil + } + + if items, err := rediscli.Values(n.Query(ctx, "SENTINEL", "MASTERS")); err != nil { + return nil, err + } else { + for _, item := range items { + node := rediscli.ParseSentinelMonitorNode(item) + if node.Name != "" { + clusters = append(clusters, node.Name) + } + } + } + return +} + +func (n *RedisSentinelNode) MonitoringNodes(ctx context.Context, name string) (master *rediscli.SentinelMonitorNode, + replicas []*rediscli.SentinelMonitorNode, err error) { + + if n == nil { + return nil, nil, nil + } + if name == "" { + return nil, nil, fmt.Errorf("empty name") + } + + if val, err := n.Query(ctx, "SENTINEL", "MASTER", name); err != nil { + if strings.Contains(err.Error(), "No such master with that name") { + return nil, nil, nil + } + return nil, nil, err + } else { + master = rediscli.ParseSentinelMonitorNode(val) + } + + // NOTE: require sentinel 5.0.0 + if items, err := rediscli.Values(n.Query(ctx, "SENTINEL", "REPLICAS", name)); err != nil { + return nil, nil, err + } else { + for _, item := range items { + node := rediscli.ParseSentinelMonitorNode(item) + replicas = append(replicas, node) + } + } + return +} + +func (n *RedisSentinelNode) Info() rediscli.RedisInfo { + if n == nil || n.info == nil { + return rediscli.RedisInfo{} + } + return *n.info +} + +func (n *RedisSentinelNode) Port() int { + if port := n.Pod.Labels[builder.PodAnnouncePortLabelKey]; port != "" { + if val, _ := strconv.ParseInt(port, 10, 32); val > 0 { + return int(val) + } + } + return n.InternalPort() +} + +func (n *RedisSentinelNode) InternalPort() int { + port := 26379 + if container := util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName); container != nil { + for _, p := range container.Ports { + if p.Name == sentinelbuilder.SentinelContainerPortName { + port = int(p.ContainerPort) + break + } + } + } + return port +} + +func (n *RedisSentinelNode) DefaultIP() net.IP { + if value := n.Pod.Labels[builder.PodAnnounceIPLabelKey]; value != "" { + address := strings.Replace(value, "-", ":", -1) + return net.ParseIP(address) + } + return n.DefaultInternalIP() +} + +func (n *RedisSentinelNode) DefaultInternalIP() net.IP { + ips := n.IPs() + if len(ips) == 0 { + return nil + } + + var ipFamilyPrefer string + if container := util.GetContainerByName(&n.Pod.Spec, sentinelbuilder.SentinelContainerName); container != nil { + for _, env := range container.Env { + if env.Name == "IP_FAMILY_PREFER" { + ipFamilyPrefer = env.Value + break + } + } + } + + if ipFamilyPrefer != "" { + for _, ip := range n.IPs() { + addr, err := netip.ParseAddr(ip.String()) + if err != nil { + continue + } + if addr.Is4() && ipFamilyPrefer == string(corev1.IPv4Protocol) || + addr.Is6() && ipFamilyPrefer == string(corev1.IPv6Protocol) { + return ip + } + } + } + return ips[0] +} + +func (n *RedisSentinelNode) IPs() []net.IP { + if n == nil { + return nil + } + ips := []net.IP{} + for _, podIp := range n.Pod.Status.PodIPs { + ips = append(ips, net.ParseIP(podIp.IP)) + } + return ips +} + +func (n *RedisSentinelNode) NodeIP() net.IP { + if n == nil { + return nil + } + return net.ParseIP(n.Pod.Status.HostIP) +} + +// ContainerStatus +func (n *RedisSentinelNode) ContainerStatus() *corev1.ContainerStatus { + if n == nil { + return nil + } + for _, status := range n.Pod.Status.ContainerStatuses { + if status.Name == sentinelbuilder.SentinelContainerName { + return &status + } + } + return nil +} + +// Status +func (n *RedisSentinelNode) Status() corev1.PodPhase { + if n == nil { + return corev1.PodUnknown + } + return n.Pod.Status.Phase +} + +func (n *RedisSentinelNode) SetMonitor(ctx context.Context, name, ip, port, user, password, quorum string) error { + if err := n.Setup(ctx, []interface{}{"SENTINEL", "REMOVE", name}); err != nil { + n.logger.Error(err, "try remove cluster failed", "name", name) + } + + n.logger.Info("set monitor", "name", name, "ip", ip, "port", port, "quorum", quorum) + if err := n.Setup(ctx, []interface{}{"SENTINEL", "MONITOR", name, ip, port, quorum}); err != nil { + return err + } + if password != "" { + if n.CurrentVersion().IsACLSupported() { + if user != "" { + if err := n.Setup(ctx, []interface{}{"SENTINEL", "SET", name, "auth-user", user}); err != nil { + return err + } + } + } + if err := n.Setup(ctx, []interface{}{"SENTINEL", "SET", name, "auth-pass", password}); err != nil { + return err + } + if err := n.Setup(ctx, []interface{}{"SENTINEL", "RESET", name}); err != nil { + return err + } + } + return nil +} diff --git a/internal/redis/sentinel/redissentinel.go b/internal/redis/sentinel/redissentinel.go new file mode 100644 index 0000000..6461458 --- /dev/null +++ b/internal/redis/sentinel/redissentinel.go @@ -0,0 +1,474 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinel + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "strconv" + "strings" + + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/api/core/helper" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/internal/builder" + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + "github.com/alauda/redis-operator/internal/util" + clientset "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/security/acl" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/alauda/redis-operator/pkg/types/user" + "github.com/go-logr/logr" + "github.com/samber/lo" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ types.RedisSentinelInstance = (*RedisSentinel)(nil) + +type RedisSentinel struct { + databasesv1.RedisSentinel + + client clientset.ClientSet + eventRecorder record.EventRecorder + + replication *RedisSentinelReplication + tlsConfig *tls.Config + users acl.Users + logger logr.Logger +} + +func NewRedisSentinel(ctx context.Context, cliset clientset.ClientSet, eventRecorder record.EventRecorder, def *databasesv1.RedisSentinel, logger logr.Logger) (*RedisSentinel, error) { + if cliset == nil { + return nil, fmt.Errorf("require clientset") + } + if def == nil { + return nil, fmt.Errorf("require sentinel instance") + } + + inst := RedisSentinel{ + RedisSentinel: *def, + + client: cliset, + eventRecorder: eventRecorder, + logger: logger.WithName("S").WithValues("instance", client.ObjectKeyFromObject(def).String()), + } + + var err error + if inst.tlsConfig, err = inst.loadTLS(ctx, inst.logger); err != nil { + inst.logger.Error(err, "load tls config failed") + return nil, err + } + if inst.users, err = inst.loadUsers(ctx, inst.logger); err != nil { + inst.logger.Error(err, "load users failed") + return nil, err + } + if inst.replication, err = NewRedisSentinelReplication(ctx, cliset, &inst, inst.logger); err != nil { + inst.logger.Error(err, "create replication failed") + return nil, err + } + return &inst, nil +} + +func (s *RedisSentinel) loadUsers(ctx context.Context, logger logr.Logger) (acl.Users, error) { + if s == nil { + return nil, fmt.Errorf("nil sentinel instance") + } + passwordSecret := s.Definition().Spec.PasswordSecret + if passwordSecret != "" { + if secret, err := s.client.GetSecret(ctx, s.GetNamespace(), passwordSecret); err != nil { + logger.Error(err, "load password secret failed", "name", passwordSecret) + return nil, err + } else if u, err := user.NewSentinelUser("", user.RoleDeveloper, secret); err != nil { + logger.Error(err, "create user failed", "name", passwordSecret) + return nil, err + } else { + return acl.Users{u}, nil + } + } + u, _ := user.NewSentinelUser("", user.RoleDeveloper, nil) + // return default user with out password + return acl.Users{u}, nil +} + +func (s *RedisSentinel) loadTLS(ctx context.Context, logger logr.Logger) (*tls.Config, error) { + if s == nil { + return nil, fmt.Errorf("nil sentinel instance") + } + + if !s.Spec.EnableTLS || s.Status.TLSSecret == "" { + return nil, nil + } + secretName := s.Status.TLSSecret + + if secret, err := s.client.GetSecret(ctx, s.GetNamespace(), secretName); err != nil { + logger.Error(err, "secret not found", "name", secretName) + return nil, err + } else if secret.Data[corev1.TLSCertKey] == nil || secret.Data[corev1.TLSPrivateKeyKey] == nil || + secret.Data["ca.crt"] == nil { + + logger.Error(fmt.Errorf("invalid tls secret"), "tls secret is invaid") + return nil, fmt.Errorf("tls secret is invalid") + } else { + cert, err := tls.X509KeyPair(secret.Data[corev1.TLSCertKey], secret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + logger.Error(err, "generate X509KeyPair failed") + return nil, err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(secret.Data["ca.crt"]) + + return &tls.Config{ + InsecureSkipVerify: true, // #nosec + RootCAs: caCertPool, + Certificates: []tls.Certificate{cert}, + }, nil + } +} + +func (s *RedisSentinel) NamespacedName() client.ObjectKey { + if s == nil { + return client.ObjectKey{} + } + return client.ObjectKey{Namespace: s.GetNamespace(), Name: s.GetName()} +} + +func (s *RedisSentinel) Arch() redis.RedisArch { + return core.RedisStdSentinel +} + +func (s *RedisSentinel) Version() redis.RedisVersion { + if s == nil || s.Spec.Image == "" { + return redis.RedisVersionUnknown + } + ver, _ := redis.ParseRedisVersionFromImage(s.Spec.Image) + return ver +} + +func (s *RedisSentinel) Definition() *databasesv1.RedisSentinel { + if s == nil { + return nil + } + return &s.RedisSentinel +} + +// TODO: implement acl when redis 5.0 deprecated and import redis 8 or valkey 8 +func (s *RedisSentinel) Users() acl.Users { + return nil +} + +func (s *RedisSentinel) Replication() types.RedisSentinelReplication { + return s.replication +} + +func (s *RedisSentinel) TLSConfig() *tls.Config { + if s == nil { + return nil + } + return s.tlsConfig +} + +func (s *RedisSentinel) Nodes() []redis.RedisSentinelNode { + if s == nil || s.replication == nil { + return nil + } + return s.replication.Nodes() +} + +func (s *RedisSentinel) RawNodes(ctx context.Context) ([]corev1.Pod, error) { + if s == nil { + return nil, nil + } + // get pods according to statefulset + name := sentinelbuilder.GetSentinelStatefulSetName(s.GetName()) + sts, err := s.client.GetStatefulSet(ctx, s.GetNamespace(), name) + if errors.IsNotFound(err) { + return nil, nil + } else if err != nil { + s.logger.Info("load statefulset failed", "name", s.GetNamespace()) + return nil, err + } + + // load pods by statefulset selector + ret, err := s.client.GetStatefulSetPodsByLabels(ctx, sts.GetNamespace(), sts.Spec.Selector.MatchLabels) + if err != nil { + s.logger.Error(err, "loads pods of sentinel statefulset failed") + return nil, err + } + return ret.Items, nil +} + +func (s *RedisSentinel) Selector() map[string]string { + // TODO: delete this method + return sentinelbuilder.GenerateSelectorLabels("sentinel", s.GetName()) +} + +func (s *RedisSentinel) Restart(ctx context.Context, annotationKeyVal ...string) error { + // update all shards + logger := s.logger.WithName("Restart") + + if s.replication != nil { + logger.V(3).Info("restart replication", "target", s.replication.NamespacedName) + return s.replication.Restart(ctx, annotationKeyVal...) + } + return nil +} + +func (s *RedisSentinel) Refresh(ctx context.Context) error { + logger := s.logger.WithName("Refresh") + + var err error + if s.replication != nil { + logger.V(3).Info("refresh replication", "target", s.replication.NamespacedName) + if err = s.replication.Refresh(ctx); err != nil { + logger.Error(err, "refresh replication failed") + return err + } + } + return nil +} + +func (s *RedisSentinel) IsReady() bool { + if s == nil || s.replication == nil { + return false + } + return s.replication.IsReady() +} + +func (s *RedisSentinel) IsInService() bool { + if s == nil || s.replication == nil { + return false + } + readyReplicas := s.replication.Status().ReadyReplicas + return readyReplicas >= (s.Spec.Replicas/2)+1 +} + +func (s *RedisSentinel) IsResourceFullfilled(ctx context.Context) (bool, error) { + if s == nil { + return false, fmt.Errorf("nil sentinel instance") + } + var ( + serviceKey = corev1.SchemeGroupVersion.WithKind("Service") + stsKey = appsv1.SchemeGroupVersion.WithKind("StatefulSet") + ) + resources := map[schema.GroupVersionKind][]string{ + serviceKey: { + sentinelbuilder.GetSentinelServiceName(s.GetName()), // rfs- + sentinelbuilder.GetSentinelHeadlessServiceName(s.GetName()), // rfs--hl + }, + stsKey: { + sentinelbuilder.GetSentinelStatefulSetName(s.GetName()), + }, + } + if s.Spec.Expose.ServiceType == corev1.ServiceTypeLoadBalancer || + s.Spec.Expose.ServiceType == corev1.ServiceTypeNodePort { + for i := 0; i < int(s.Spec.Replicas); i++ { + svcName := sentinelbuilder.GetSentinelNodeServiceName(s.GetName(), i) + resources[serviceKey] = append(resources[serviceKey], svcName) + } + } + + for gvk, names := range resources { + for _, name := range names { + var obj unstructured.Unstructured + obj.SetGroupVersionKind(gvk) + + err := s.client.Client().Get(ctx, client.ObjectKey{Namespace: s.GetNamespace(), Name: name}, &obj) + if errors.IsNotFound(err) { + s.logger.V(3).Info("resource not found", "target", util.ObjectKey(s.GetNamespace(), name)) + return false, nil + } else if err != nil { + s.logger.Error(err, "get resource failed", "target", util.ObjectKey(s.GetNamespace(), name)) + return false, err + } + // if gvk == stsKey { + // if replicas, found, err := unstructured.NestedInt64(obj.Object, "spec", "replicas"); err != nil { + // s.logger.Error(err, "get service replicas failed", "target", util.ObjectKey(s.GetNamespace(), name)) + // return false, err + // } else if found && replicas != int64(s.Spec.Replicas) { + // s.logger.Info("@@@@@@@ found", "replicas", replicas, "s.Spec.Replicas", s.Spec.Replicas) + // return false, nil + // } + // } + } + } + return true, nil +} + +func (s *RedisSentinel) GetPassword() (string, error) { + if s == nil { + return "", nil + } + if s.Spec.PasswordSecret == "" { + return "", nil + } + secret, err := s.client.GetSecret(context.Background(), s.GetNamespace(), s.Spec.PasswordSecret) + if err != nil { + return "", err + } + return string(secret.Data["password"]), nil +} + +func (s *RedisSentinel) UpdateStatus(ctx context.Context, st types.InstanceStatus, msg string) error { + if s == nil { + return fmt.Errorf("nil sentinel instance") + } + + var ( + status = s.RedisSentinel.Status.DeepCopy() + sen = lo.IfF(s.replication != nil, func() *appsv1.StatefulSetStatus { + return s.replication.Status() + }).Else(nil) + nodeports = map[int32]struct{}{} + ) + switch st { + case types.OK: + status.Phase = databasesv1.SentinelReady + case types.Fail: + status.Phase = databasesv1.SentinelFail + case types.Paused: + status.Phase = databasesv1.SentinelPaused + default: + status.Phase = databasesv1.SentinelCreating + } + status.Message = msg + + status.Nodes = status.Nodes[:0] + for _, node := range s.Nodes() { + n := core.RedisNode{ + Role: "Sentinel", + IP: node.DefaultIP().String(), + Port: fmt.Sprintf("%d", node.Port()), + PodName: node.GetName(), + StatefulSet: s.replication.GetName(), + NodeName: node.NodeIP().String(), + } + status.Nodes = append(status.Nodes, n) + if port := node.Definition().Labels[builder.PodAnnouncePortLabelKey]; port != "" { + val, _ := strconv.ParseInt(port, 10, 32) + nodeports[int32(val)] = struct{}{} + } + } + + phase, msg := func() (databasesv1.SentinelPhase, string) { + // use passed status if provided + if status.Phase == databasesv1.SentinelFail || status.Phase == databasesv1.SentinelPaused { + return status.Phase, status.Message + } + + // check creating + if sen == nil || sen.CurrentReplicas != s.Definition().Spec.Replicas || + sen.Replicas != s.Definition().Spec.Replicas { + return databasesv1.SentinelCreating, "" + } + + var pendingPods []string + // check pending + for _, node := range s.Nodes() { + for _, cond := range node.Definition().Status.Conditions { + if cond.Type == corev1.PodScheduled && cond.Status == corev1.ConditionFalse { + pendingPods = append(pendingPods, node.GetName()) + } + } + } + if len(pendingPods) > 0 { + return databasesv1.SentinelCreating, fmt.Sprintf("pods %s pending", strings.Join(pendingPods, ",")) + } + + // check nodeport applied + if seq := s.Spec.Expose.NodePortSequence; s.Spec.Expose.ServiceType == corev1.ServiceTypeNodePort { + var ( + notAppliedPorts = []string{} + customPorts, _ = helper.ParseSequencePorts(seq) + ) + for _, port := range customPorts { + if _, ok := nodeports[port]; !ok { + notAppliedPorts = append(notAppliedPorts, strconv.Itoa(int(port))) + } + } + if len(notAppliedPorts) > 0 { + return databasesv1.SentinelCreating, fmt.Sprintf("nodeport %s not applied", strings.Join(notAppliedPorts, ",")) + } + } + + // make sure all is ready + if sen != nil && + sen.ReadyReplicas == s.Definition().Spec.Replicas && + sen.CurrentReplicas == sen.ReadyReplicas && + sen.CurrentRevision == sen.UpdateRevision { + + return databasesv1.SentinelReady, "" + } + + var notReadyPods []string + for _, node := range s.Nodes() { + if !node.IsReady() { + notReadyPods = append(notReadyPods, node.GetName()) + } + } + if len(notReadyPods) > 0 { + return databasesv1.SentinelCreating, fmt.Sprintf("pods %s not ready", strings.Join(notReadyPods, ",")) + } + return databasesv1.SentinelCreating, "" + }() + status.Phase, status.Message = phase, lo.If(msg == "", status.Message).Else(msg) + + // update status + s.RedisSentinel.Status = *status + if err := s.client.UpdateRedisSentinelStatus(ctx, &s.RedisSentinel); err != nil { + s.logger.Error(err, "update RedisFailover status failed") + return err + } + return nil +} + +func (s *RedisSentinel) IsACLAppliedToAll() bool { + // TODO: implement acl when redis 5.0 deprecated and import redis 8 or valkey 8 + return false +} + +func (s *RedisSentinel) IsACLUserExists() bool { + // TODO: implement acl when redis 5.0 deprecated and import redis 8 or valkey 8 + return false +} + +func (s *RedisSentinel) IsACLApplied() bool { + // TODO: implement acl when redis 5.0 deprecated and import redis 8 or valkey 8 + return false +} + +func (c *RedisSentinel) Logger() logr.Logger { + if c == nil { + return logr.Discard() + } + return c.logger +} + +func (c *RedisSentinel) SendEventf(eventtype, reason, messageFmt string, args ...interface{}) { + if c == nil { + return + } + c.eventRecorder.Eventf(c.Definition(), eventtype, reason, messageFmt, args...) +} diff --git a/internal/redis/sentinel/replication.go b/internal/redis/sentinel/replication.go new file mode 100644 index 0000000..7c02945 --- /dev/null +++ b/internal/redis/sentinel/replication.go @@ -0,0 +1,163 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sentinel + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/alauda/redis-operator/internal/builder/sentinelbuilder" + "github.com/alauda/redis-operator/internal/util" + clientset "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" + appv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/errors" + k8stypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ types.RedisSentinelReplication = (*RedisSentinelReplication)(nil) + +type RedisSentinelReplication struct { + appv1.StatefulSet + client clientset.ClientSet + instance types.RedisSentinelInstance + nodes []redis.RedisSentinelNode + logger logr.Logger +} + +func NewRedisSentinelReplication(ctx context.Context, client clientset.ClientSet, inst types.RedisSentinelInstance, logger logr.Logger) (*RedisSentinelReplication, error) { + if client == nil { + return nil, fmt.Errorf("require clientset") + } + if inst == nil { + return nil, fmt.Errorf("require sentinel instance") + } + + name := sentinelbuilder.GetSentinelStatefulSetName(inst.GetName()) + sts, err := client.GetStatefulSet(ctx, inst.GetNamespace(), name) + if errors.IsNotFound(err) { + return nil, nil + } else if err != nil { + logger.Info("load deployment failed", "name", name) + return nil, err + } + + node := RedisSentinelReplication{ + StatefulSet: *sts, + client: client, + instance: inst, + logger: logger.WithName("RedisSentinelReplication"), + } + if node.nodes, err = LoadRedisSentinelNodes(ctx, client, sts, inst.Users().GetOpUser(), logger); err != nil { + + logger.Error(err, "load shard nodes failed", "shard", sts.GetName()) + return nil, err + } + return &node, nil +} + +func (s *RedisSentinelReplication) NamespacedName() client.ObjectKey { + if s == nil { + return client.ObjectKey{} + } + return client.ObjectKey{ + Namespace: s.GetNamespace(), + Name: s.GetName(), + } +} + +func (s *RedisSentinelReplication) Version() redis.RedisVersion { + if s == nil { + return redis.RedisVersionUnknown + } + container := util.GetContainerByName(&s.Spec.Template.Spec, sentinelbuilder.SentinelContainerName) + ver, _ := redis.ParseRedisVersionFromImage(container.Image) + return ver +} + +func (s *RedisSentinelReplication) Definition() *appv1.StatefulSet { + if s == nil { + return nil + } + return &s.StatefulSet +} + +func (s *RedisSentinelReplication) Nodes() []redis.RedisSentinelNode { + if s == nil { + return nil + } + return s.nodes +} + +func (s *RedisSentinelReplication) Restart(ctx context.Context, annotationKeyVal ...string) error { + // update all shards + logger := s.logger.WithName("Restart") + + kv := map[string]string{ + "kubectl.kubernetes.io/restartedAt": time.Now().Format(time.RFC3339Nano), + } + for i := 0; i < len(annotationKeyVal)-1; i += 2 { + kv[annotationKeyVal[i]] = annotationKeyVal[i+1] + } + + data, _ := json.Marshal(map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": kv, + }, + }, + }, + }) + + if err := s.client.Client().Patch(ctx, &s.StatefulSet, + client.RawPatch(k8stypes.StrategicMergePatchType, data)); err != nil { + logger.Error(err, "restart deployment failed", "target", client.ObjectKeyFromObject(&s.StatefulSet)) + return err + } + return nil +} + +func (s *RedisSentinelReplication) IsReady() bool { + if s == nil { + return false + } + return s.Status().ReadyReplicas == *s.Spec.Replicas && s.Status().UpdateRevision == s.Status().CurrentRevision +} + +func (s *RedisSentinelReplication) Refresh(ctx context.Context) error { + logger := s.logger.WithName("Refresh") + + var err error + if s.nodes, err = LoadRedisSentinelNodes(ctx, s.client, &s.StatefulSet, s.instance.Users().GetOpUser(), logger); err != nil { + logger.Error(err, "load shard nodes failed", "shard", s.GetName()) + return err + } + return nil +} + +func (s *RedisSentinelReplication) Status() *appv1.StatefulSetStatus { + if s == nil { + return nil + } + return &s.StatefulSet.Status +} diff --git a/pkg/util/auth.go b/internal/util/auth.go similarity index 75% rename from pkg/util/auth.go rename to internal/util/auth.go index ab64884..d26da1d 100644 --- a/pkg/util/auth.go +++ b/internal/util/auth.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,12 +18,15 @@ package util import ( "crypto/tls" + "crypto/x509" + "errors" "fmt" "regexp" + "slices" "strings" "unicode" - "k8s.io/utils/strings/slices" + corev1 "k8s.io/api/core/v1" ) type AuthConfig struct { @@ -31,6 +34,29 @@ type AuthConfig struct { TLSConfig *tls.Config } +func LoadCertConfigFromSecret(secret *corev1.Secret) (*tls.Config, error) { + if secret == nil { + return nil, errors.New("tls secret is nil") + } + + if secret.Data[corev1.TLSCertKey] == nil || secret.Data[corev1.TLSPrivateKeyKey] == nil || + secret.Data["ca.crt"] == nil { + return nil, fmt.Errorf("tls secret is invalid") + } + cert, err := tls.X509KeyPair(secret.Data[corev1.TLSCertKey], secret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + return nil, err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(secret.Data["ca.crt"]) + + return &tls.Config{ + InsecureSkipVerify: true, // #nosec + RootCAs: caCertPool, + Certificates: []tls.Certificate{cert}, + }, nil +} + var ( redisACLCategoryReg = regexp.MustCompile(`\s[+\-]@(\S+)`) ) @@ -66,19 +92,15 @@ func CheckRule(aclRules string) error { return fmt.Errorf("acl rule %s is not allowed", rule) } if unicode.IsLetter(rune(rule[0])) { - if !slices.Contains([]string{"on", "off", "nopass", "reset", - "resetpass", "allcommands", "nocommands", "allkeys", "resetkeys", - "allchannels", "resetchannels", "clearselectors", "sanitize-payload", - "skip-sanitize-payload"}, rule) { + if !slices.Contains([]string{"allcommands", "nocommands", "allkeys", "allchannels"}, rule) { return fmt.Errorf("acl rule %s is not allowed", rule) } } else { //如果 不是以 &+->~% 开头报错 - if !slices.Contains([]string{"&", "+", "-", ">", "<", "~", "%"}, string(rule[0])) { + if !slices.Contains([]string{"&", "+", "-", ">", "<", "~", "%", "|"}, string(rule[0])) { return fmt.Errorf("acl rule %s is not allowed", rule) } } - } return nil } diff --git a/pkg/util/kubernetes.go b/internal/util/kubernetes.go similarity index 61% rename from pkg/util/kubernetes.go rename to internal/util/kubernetes.go index 9c3f839..e56bdc9 100644 --- a/pkg/util/kubernetes.go +++ b/internal/util/kubernetes.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,31 +17,31 @@ limitations under the License. package util import ( + "fmt" + "strconv" + "strings" "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" ) -type Object interface { - GetObjectKind() schema.ObjectKind - metav1.Object -} - func BuildOwnerReferences(obj client.Object) (refs []metav1.OwnerReference) { if obj != nil { if obj.GetObjectKind().GroupVersionKind().Kind != "" && obj.GetName() != "" && obj.GetUID() != "" { refs = append(refs, metav1.OwnerReference{ - APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), - Kind: obj.GetObjectKind().GroupVersionKind().Kind, - Name: obj.GetName(), - UID: obj.GetUID(), + APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), + Kind: obj.GetObjectKind().GroupVersionKind().Kind, + Name: obj.GetName(), + UID: obj.GetUID(), + Controller: pointer.Bool(true), + BlockOwnerDeletion: pointer.Bool(true), }) } else { refs = append(refs, obj.GetOwnerReferences()...) @@ -50,19 +50,29 @@ func BuildOwnerReferences(obj client.Object) (refs []metav1.OwnerReference) { return } +func ObjectKey(namespace, name string) client.ObjectKey { + return client.ObjectKey{ + Namespace: namespace, + Name: name, + } +} + func BuildOwnerReferencesWithParents(obj client.Object) (refs []metav1.OwnerReference) { if obj != nil { if obj.GetObjectKind().GroupVersionKind().Kind != "" && obj.GetName() != "" && obj.GetUID() != "" { refs = append(refs, metav1.OwnerReference{ - APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), - Kind: obj.GetObjectKind().GroupVersionKind().Kind, - Name: obj.GetName(), - UID: obj.GetUID(), + APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), + Kind: obj.GetObjectKind().GroupVersionKind().Kind, + Name: obj.GetName(), + UID: obj.GetUID(), + BlockOwnerDeletion: pointer.Bool(true), + Controller: pointer.Bool(true), }) } - if obj.GetOwnerReferences() != nil { - refs = append(refs, obj.GetOwnerReferences()...) + for _, ref := range obj.GetOwnerReferences() { + ref.Controller = nil + refs = append(refs, ref) } } return @@ -86,6 +96,18 @@ func GetContainerByName(pod *corev1.PodSpec, name string) *corev1.Container { return nil } +func GetServicePortByName(svc *corev1.Service, name string) *corev1.ServicePort { + if svc == nil { + return nil + } + for i := range svc.Spec.Ports { + if svc.Spec.Ports[i].Name == name { + return &svc.Spec.Ports[i] + } + } + return nil +} + // GetVolumeClaimTemplatesByName func GetVolumeClaimTemplatesByName(vols []corev1.PersistentVolumeClaim, name string) *corev1.PersistentVolumeClaim { for _, vol := range vols { @@ -117,3 +139,14 @@ func RetryOnTimeout(f func() error, step int) error { errors.IsTooManyRequests(err) || errors.IsServiceUnavailable(err) }, f) } + +func ParsePodIndex(name string) (index int, err error) { + fields := strings.Split(name, "-") + if len(fields) < 2 { + return -1, fmt.Errorf("invalid pod name %s", name) + } + if index, err = strconv.Atoi(fields[len(fields)-1]); err != nil { + return -1, fmt.Errorf("invalid pod name %s", name) + } + return index, nil +} diff --git a/internal/util/sig.go b/internal/util/sig.go new file mode 100644 index 0000000..460a8d1 --- /dev/null +++ b/internal/util/sig.go @@ -0,0 +1,64 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "crypto/sha256" + "fmt" + "slices" + "strings" + + corev1 "k8s.io/api/core/v1" +) + +type SignaturableObject interface { + string | []byte +} + +func mapSigGenerator[T SignaturableObject](obj map[string]T, salt string) (string, error) { + var ( + keys []string + data []string + ) + for key := range obj { + keys = append(keys, key) + } + slices.Sort(keys) + for _, key := range keys { + data = append(data, fmt.Sprintf("%v", obj[key])) + } + return fmt.Sprintf("%x", sha256.Sum256(append([]byte(salt), []byte(strings.Join(data, "\n"))...))), nil +} + +func GenerateObjectSig(data interface{}, salt string) (string, error) { + if data == nil { + return "", nil + } + + switch val := data.(type) { + case string: + return fmt.Sprintf("%x", sha256.Sum256(append([]byte(salt), []byte(val)...))), nil + case []byte: + return fmt.Sprintf("%x", sha256.Sum256(append([]byte(salt), val...))), nil + case *corev1.ConfigMap: + return mapSigGenerator(val.Data, salt) + case *corev1.Secret: + return mapSigGenerator(val.Data, salt) + default: + return "", fmt.Errorf("unsupported data type %T", data) + } +} diff --git a/pkg/util/unit.go b/internal/util/unit.go similarity index 97% rename from pkg/util/unit.go rename to internal/util/unit.go index a0211c5..ba0e783 100644 --- a/pkg/util/unit.go +++ b/internal/util/unit.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..26c9e0a --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,93 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "crypto/rand" + "fmt" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" +) + +func ParseRedisMemConf(p string) (string, error) { + var mul int64 = 1 + u := strings.ToLower(p) + digits := u + + if strings.HasSuffix(u, "k") { + digits = u[:len(u)-len("k")] + mul = 1000 + } else if strings.HasSuffix(u, "kb") { + digits = u[:len(u)-len("kb")] + mul = 1024 + } else if strings.HasSuffix(u, "m") { + digits = u[:len(u)-len("m")] + mul = 1000 * 1000 + } else if strings.HasSuffix(u, "mb") { + digits = u[:len(u)-len("mb")] + mul = 1024 * 1024 + } else if strings.HasSuffix(u, "g") { + digits = u[:len(u)-len("g")] + mul = 1000 * 1000 * 1000 + } else if strings.HasSuffix(u, "gb") { + digits = u[:len(u)-len("gb")] + mul = 1024 * 1024 * 1024 + } else if strings.HasSuffix(u, "b") { + digits = u[:len(u)-len("b")] + mul = 1 + } + + val, err := strconv.ParseInt(digits, 10, 64) + if err != nil { + return "", err + } + return strconv.FormatInt(val*mul, 10), nil +} + +func GenerateRedisRandPassword() string { + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + panic(err) + } + return fmt.Sprintf("%x", buf) +} + +func GenerateRedisRebuildAnnotation() map[string]string { + m := make(map[string]string) + m["middle.alauda.cn/rebuild"] = "true" + return m +} + +func GetPullPolicy(policies ...corev1.PullPolicy) corev1.PullPolicy { + for _, policy := range policies { + if policy != "" { + return policy + } + } + return corev1.PullAlways +} + +// split storage name, example: pvc/redisfailover-persistent-keep-data-rfr-redis-sentinel-demo-0 +func GetClaimName(backupDestination string) string { + names := strings.Split(backupDestination, "/") + if len(names) != 2 { + return "" + } + return names[1] +} diff --git a/pkg/util/util_test.go b/internal/util/util_test.go similarity index 59% rename from pkg/util/util_test.go rename to internal/util/util_test.go index 1173801..fc55479 100644 --- a/pkg/util/util_test.go +++ b/internal/util/util_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,100 +20,9 @@ import ( "fmt" "testing" - "github.com/alauda/redis-operator/pkg/types/slot" + "github.com/alauda/redis-operator/pkg/slot" ) -func TestCompareStrings(t *testing.T) { - tests := []struct { - a string - b string - result int - }{ - {"rfr-s-v6-0", "rfr-s-v6-1", -1}, - {"rfr-s-v6-2", "rfr-s-v6-1", 1}, - {"rfr-s-v6-10", "rfr-s-v6-10", 0}, - {"rfr-s-v6-11", "rfr-s-v6-0", 1}, - {"rfr-s-v6-15", "rfr-s-v6-5", 1}, - {"", "rfr-s-v6-0", -1}, // 测试空字符串 - {"rfr-s-v6-0", "", 1}, // 测试空字符串 - {"", "", 0}, // 测试两个空字符串 - } - - for _, test := range tests { - res := CompareStrings(test.a, test.b) - if res != test.result { - t.Errorf("CompareStrings(%s, %s) expected %d, got %d", test.a, test.b, test.result, res) - } - } -} - -func TestParsePortSequence(t *testing.T) { - testCases := []struct { - name string - portSequence string - expectedPorts []int32 - expectedError error - }{ - { - name: "Basic range", - portSequence: "1-3", - expectedPorts: []int32{1, 2, 3}, - expectedError: nil, - }, - { - name: "Single value", - portSequence: "5", - expectedPorts: []int32{5}, - expectedError: nil, - }, - { - name: "Mixed ranges and single values", - portSequence: "3,4-6,7,9-10", - expectedPorts: []int32{3, 4, 5, 6, 7, 9, 10}, - expectedError: nil, - }, - { - name: "Invalid format", - portSequence: "4-6,7-", - expectedPorts: nil, - expectedError: fmt.Errorf("strconv.Atoi: parsing \"\": invalid syntax"), - }, - { - name: "Unsorted and overlapping", - portSequence: "9-10,7,4-6,3,5-8", - expectedPorts: []int32{3, 4, 5, 6, 7, 8, 9, 10}, - expectedError: nil, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ports, err := ParsePortSequence(tc.portSequence) - - if tc.expectedError != nil && err == nil { - t.Errorf("Expected error, got nil") - } - - if tc.expectedError == nil && err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if tc.expectedError != nil && err != nil && tc.expectedError.Error() != err.Error() { - t.Errorf("Expected error '%v', got '%v'", tc.expectedError, err) - } - - if len(tc.expectedPorts) != len(ports) { - t.Errorf("Expected ports length %v, got %v", len(tc.expectedPorts), len(ports)) - } - - for i, port := range tc.expectedPorts { - if port != ports[i] { - t.Errorf("Expected port %v at position %v, got %v", port, i, ports[i]) - } - } - }) - } -} - func TestSlot(t *testing.T) { // for update validator, only check slots fullfilled var ( @@ -135,8 +44,8 @@ func TestSlot(t *testing.T) { if !fullSlots.IsFullfilled() { t.Errorf("specified shard slots not fullfilled all slots") } - if total == 16384 { - t.Errorf("shard slots must duplicated") + if total <= 16384 { + t.Errorf("specified shard slots should be duplicated") } } diff --git a/internal/vc/generator.go b/internal/vc/generator.go new file mode 100644 index 0000000..02b2658 --- /dev/null +++ b/internal/vc/generator.go @@ -0,0 +1,504 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vc + +import ( + "context" + "crypto/md5" // nolint:gosec + stderrs "errors" + "fmt" + "os" + "reflect" + "slices" + "sort" + "strings" + + clusterv1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + "github.com/alauda/redis-operator/api/core" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + middlewarev1 "github.com/alauda/redis-operator/api/middleware/v1" + "github.com/alauda/redis-operator/internal/config" + vc "github.com/alauda/redis-operator/internal/vc/v1" + + "github.com/Masterminds/semver/v3" + "github.com/go-logr/logr" + appv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GenerateRedisBundleImageVersion(version string) string { + return fmt.Sprintf("redis-artifact-%s", version) +} + +func ParseImageAndTag(image string) (string, string, error) { + fields := strings.Split(image, ":") + if len(fields) != 2 { + return "", "", fmt.Errorf("invalid image %s", image) + } + return fields[0], fields[1], nil +} + +func GetImageNameAndTagFromEnv(name string) (string, string, error) { + val := os.Getenv(name) + if val == "" { + return "", "", fmt.Errorf("env %s is empty", name) + } + return ParseImageAndTag(val) +} + +func appendComponentVersion(comp *vc.Component, imagetag, version, displayVersion string) error { + if image, tag, err := ParseImageAndTag(imagetag); err != nil { + return err + } else { + if version != "" { + version = fmt.Sprintf("%s.0", version) + } + if !slices.ContainsFunc(comp.ComponentVersions, func(vc vc.ComponentVersion) bool { + return vc.Tag == tag + }) { + comp.ComponentVersions = append(comp.ComponentVersions, vc.ComponentVersion{ + Image: image, + Tag: tag, + Version: version, + DisplayVersion: displayVersion, + }) + } + } + return nil +} + +// GenerateOldImageVersion generate the old version of ImageVersion +// this function only fetch three images: redis,redis-exporter,redis-tools +// images of redisbackup,redisproxy,redisshake will be ignored +func GenerateOldImageVersion(ctx context.Context, cli client.Client, namespace string, logger logr.Logger) (*vc.ImageVersion, error) { + var ( + operatorVersion string + listOptions = client.ListOptions{ + Namespace: namespace, + Limit: 16, + } + comps = map[string]*vc.Component{ + "redis": {CoreComponent: true}, + "activeredis": {}, + "redis-exporter": {}, + "redis-tools": {}, + "redis-shake": {}, + "redis-proxy": {}, + "expose-pod": {}, + } + ) + + for { + var ret middlewarev1.RedisList + if err := cli.List(ctx, &ret, &listOptions); err != nil { + logger.Error(err, "failed to list redisfailover") + return nil, fmt.Errorf("failed to list redisfailover: %w", err) + } + + for _, obj := range ret.Items { + item := obj.DeepCopy() + if ov := item.Annotations[config.OperatorVersionAnnotation]; strings.HasPrefix(ov, "v3.14.") || + strings.HasPrefix(ov, "v3.15.") || + strings.HasPrefix(ov, "v3.16.") { + if operatorVersion == "" { + operatorVersion = ov + } + } else if ov := item.Status.UpgradeStatus.CRVersion; strings.HasPrefix(ov, "3.14.") || + strings.HasPrefix(ov, "3.15.") || + strings.HasPrefix(ov, "3.16.") { + if operatorVersion == "" { + operatorVersion = ov + } + } else { + continue + } + + version := item.Spec.Version + stsName := fmt.Sprintf("rfr-%s", item.Name) + switch item.Spec.Arch { + case core.RedisSentinel: + var inst databasesv1.RedisFailover + if err := cli.Get(ctx, client.ObjectKeyFromObject(item), &inst); err != nil { + logger.Error(err, "failed to get redisfailover", "redisfailover", client.ObjectKeyFromObject(item)) + continue + } + + if inst.Spec.EnableActiveRedis { + if err := appendComponentVersion(comps["activeredis"], inst.Spec.Redis.Image, version, version); err != nil { + logger.Error(err, "failed to load activeredis image", "container", inst.Spec.Redis.Image) + } + } else { + if err := appendComponentVersion(comps["redis"], inst.Spec.Redis.Image, version, version); err != nil { + logger.Error(err, "failed to load redis image", "container", inst.Spec.Redis.Image) + } + } + if inst.Spec.Redis.Exporter.Image != "" { + if err := appendComponentVersion(comps["redis-exporter"], inst.Spec.Redis.Exporter.Image, "", ""); err != nil { + logger.Error(err, "failed to load redis-exporter image", "container", inst.Spec.Redis.Exporter.Image) + } + } + stsName = fmt.Sprintf("rfr-%s", item.Name) + case core.RedisCluster: + var inst clusterv1alpha1.DistributedRedisCluster + if err := cli.Get(ctx, client.ObjectKeyFromObject(item), &inst); err != nil { + logger.Error(err, "failed to get rediscluster", "rediscluster", client.ObjectKeyFromObject(item)) + continue + } + + if inst.Spec.EnableActiveRedis { + if err := appendComponentVersion(comps["activeredis"], inst.Spec.Image, version, version); err != nil { + logger.Error(err, "failed to load activeredis image", "container", inst.Spec.Image) + } + } else { + if err := appendComponentVersion(comps["redis"], inst.Spec.Image, version, version); err != nil { + logger.Error(err, "failed to load redis image", "container", inst.Spec.Image) + } + } + if inst.Spec.Monitor != nil && inst.Spec.Monitor.Image != "" { + if err := appendComponentVersion(comps["redis-exporter"], inst.Spec.Monitor.Image, "", ""); err != nil { + logger.Error(err, "failed to load redis-exporter image", "container", inst.Spec.Monitor.Image) + } + } + stsName = fmt.Sprintf("drc-%s-0", item.Name) + } + + if stsName != "" { + // get statefulset of redisfailover + var sts appv1.StatefulSet + if err := cli.Get(ctx, client.ObjectKey{ + Namespace: item.Namespace, + Name: stsName, + }, &sts); err != nil { + logger.Error(err, "failed to list statefulset", "instance", client.ObjectKeyFromObject(item)) + continue + } + for _, container := range append(append([]corev1.Container{}, sts.Spec.Template.Spec.InitContainers...), sts.Spec.Template.Spec.Containers...) { + if container.Image == "" { + continue + } + if strings.Contains(container.Image, "redis_exporter") { + if err := appendComponentVersion(comps["redis-exporter"], container.Image, "", ""); err != nil { + logger.Error(err, "failed to load redis-exporter image", "container", container.Image) + } + } else if strings.Contains(container.Image, "redis-tools") { + if err := appendComponentVersion(comps["redis-tools"], container.Image, "", ""); err != nil { + logger.Error(err, "failed to load redis-tools image", "container", container.Image) + } + } else if strings.Contains(container.Image, "expose_pod") { + // sentinel 3.14 used only + if err := appendComponentVersion(comps["expose-pod"], container.Image, "", ""); err != nil { + logger.Error(err, "failed to load expose-pod image", "container", container.Image) + } + } + } + } + } + + if ret.Continue == "" { + break + } + listOptions.Continue = ret.Continue + } + + if operatorVersion == "" { + return nil, ErrBundleVersionNotFound + } + + ver, err := semver.NewVersion(operatorVersion) + if err != nil { + logger.Error(err, "failed to parse operator version", "version", operatorVersion) + return nil, fmt.Errorf("failed to parse operator version: %w", err) + } + + iv := vc.ImageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + config.InstanceTypeKey: config.CoreComponentName, + config.CRVersionKey: ver.String(), + }, + Name: GenerateRedisBundleImageVersion(ver.String()), + }, + Spec: vc.ImageVersionSpec{ + CrVersion: ver.String(), + Components: map[string]vc.Component{}, + }, + } + for name, comp := range comps { + logger.Info("component", "name", name, "comp", comp, "version", operatorVersion) + if len(comp.ComponentVersions) == 0 { + continue + } + iv.Spec.Components[name] = *comp + } + return &iv, nil +} + +func GenerateCurrentImageVersion(ctx context.Context, cli client.Client, logger logr.Logger) (*vc.ImageVersion, error) { + operatorVersion := config.GetOperatorVersion() + if operatorVersion == "" { + logger.Error(fmt.Errorf("operator version is empty"), "operator version is empty") + return nil, fmt.Errorf("operator version is empty") + } + ver, err := semver.NewVersion(operatorVersion) + if err != nil { + logger.Error(err, "failed to parse operator version", "operator version", operatorVersion) + return nil, fmt.Errorf("failed to parse operator version: %w", err) + } + + redisImage5, redisImage5Tag, err := GetImageNameAndTagFromEnv("REDIS_VERSION_5_IMAGE") + if err != nil { + logger.Error(err, "failed to get redis image 5") + return nil, fmt.Errorf("failed to get redis image 5: %w", err) + } + redisImage5Version := os.Getenv("REDIS_VERSION_5_VERSION") + + redisImage6, redisImage6Tag, err := GetImageNameAndTagFromEnv("REDIS_VERSION_6_IMAGE") + if err != nil { + logger.Error(err, "failed to get redis image 6") + return nil, fmt.Errorf("failed to get redis image 6: %w", err) + } + redisImage6Version := os.Getenv("REDIS_VERSION_6_VERSION") + + redisImage7, redisImage7Tag, err := GetImageNameAndTagFromEnv("REDIS_VERSION_7_2_IMAGE") + if err != nil { + logger.Error(err, "failed to get redis image 7.2") + return nil, fmt.Errorf("failed to get redis image 7.2: %w", err) + } + redisImage7Version := os.Getenv("REDIS_VERSION_7_2_VERSION") + + toolsImage, toolsImageTag, err := GetImageNameAndTagFromEnv("REDIS_TOOLS_IMAGE") + if err != nil { + logger.Error(err, "failed to get redis tools image") + return nil, fmt.Errorf("failed to get redis tools image: %w", err) + } + exporterImage, exporterImageTag, err := GetImageNameAndTagFromEnv("DEFAULT_EXPORTER_IMAGE") + if err != nil { + logger.Error(err, "failed to get redis exporter image") + return nil, fmt.Errorf("failed to get redis exporter image: %w", err) + } + + iv := vc.ImageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + config.InstanceTypeKey: config.CoreComponentName, + config.CRVersionKey: ver.String(), + }, + Name: GenerateRedisBundleImageVersion(ver.String()), + }, + Spec: vc.ImageVersionSpec{ + CrVersion: ver.String(), + Components: map[string]vc.Component{ + "redis": { + CoreComponent: true, + ComponentVersions: []vc.ComponentVersion{ + {Image: redisImage5, Tag: redisImage5Tag, Version: redisImage5Version, DisplayVersion: "5.0"}, + {Image: redisImage6, Tag: redisImage6Tag, Version: redisImage6Version, DisplayVersion: "6.0"}, + {Image: redisImage7, Tag: redisImage7Tag, Version: redisImage7Version, DisplayVersion: "7.2"}, + }, + }, + "redis-exporter": { + ComponentVersions: []vc.ComponentVersion{ + {Image: exporterImage, Tag: exporterImageTag}, + }, + }, + "redis-tools": { + ComponentVersions: []vc.ComponentVersion{ + {Image: toolsImage, Tag: toolsImageTag}, + }, + }, + }, + }, + } + return &iv, nil +} + +func CreateOrUpdateImageVersion(ctx context.Context, cli client.Client, iv *vc.ImageVersion) error { + var rawdata []string + for _, comp := range iv.Spec.Components { + for _, version := range comp.ComponentVersions { + rawdata = append(rawdata, fmt.Sprintf("%s:%s", version.Image, version.Tag)) + } + } + sort.Strings(rawdata) + + hashsum := fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(rawdata, ",")))) // nolint:gosec + if iv.Labels == nil { + iv.Labels = map[string]string{} + } + iv.Labels[config.CRVersionSHAKey] = hashsum + + var oldIV vc.ImageVersion + if err := cli.Get(ctx, client.ObjectKeyFromObject(iv), &oldIV); errors.IsNotFound(err) { + if err := cli.Create(ctx, iv); errors.IsAlreadyExists(err) { + return nil + } else if err != nil { + return err + } + } else if err != nil { + return err + } else if !reflect.DeepEqual(oldIV.Spec, iv.Spec) || !reflect.DeepEqual(oldIV.Labels, iv.Labels) { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldIV vc.ImageVersion + if err := cli.Get(ctx, client.ObjectKeyFromObject(iv), &oldIV); err != nil { + return err + } + iv.ResourceVersion = oldIV.ResourceVersion + return cli.Update(ctx, iv) + }) + } + return nil +} + +func CleanImageVersions(ctx context.Context, cli client.Client, logger logr.Logger) error { + var ret vc.ImageVersionList + if err := cli.List(ctx, &ret, client.MatchingLabels{config.InstanceTypeKey: config.CoreComponentName}); err != nil { + logger.Error(err, "failed to list image version") + return fmt.Errorf("failed to list image version: %w", err) + } + var ( + minorVersions []string + ivGroup = map[string][]*vc.ImageVersion{} + latest *vc.ImageVersion + latestVersion *semver.Version + ) + for _, item := range ret.Items { + ver, err := semver.NewVersion(item.Spec.CrVersion) + if err != nil { + logger.Error(err, "failed to parse version, deleted", "version", item.Spec.CrVersion) + if err := cli.Delete(ctx, item.DeepCopy()); err != nil { + logger.Error(err, "failed to delete image version", "image version", item) + return fmt.Errorf("failed to delete image version: %w", err) + } + continue + } + key := fmt.Sprintf("%d.%d.0", ver.Major(), ver.Minor()) + ivGroup[key] = append(ivGroup[key], item.DeepCopy()) + if !slices.Contains(minorVersions, key) { + minorVersions = append(minorVersions, key) + } + if latestVersion == nil || ver.GreaterThan(latestVersion) { + latestVersion = ver + latest = item.DeepCopy() + } + } + + sort.SliceStable(minorVersions, func(i, j int) bool { + left := semver.MustParse(minorVersions[i]) + right := semver.MustParse(minorVersions[j]) + return left.GreaterThan(right) + }) + + for key, items := range ivGroup { + sort.SliceStable(items, func(i, j int) bool { + iv := semver.MustParse(ret.Items[i].Spec.CrVersion) + jv := semver.MustParse(ret.Items[j].Spec.CrVersion) + return iv.GreaterThan(jv) + }) + ivGroup[key] = items + } + + for i, key := range minorVersions { + if i >= 3 { + for _, item := range ivGroup[key] { + if err := cli.Delete(ctx, item); err != nil { + logger.Error(err, "failed to delete image version", "image version", item) + return fmt.Errorf("failed to delete image version: %w", err) + } + } + continue + } + for j, obj := range ivGroup[key] { + item := obj.DeepCopy() + // only keep 3 newest versions for each minor version + if j >= 3 { + if err := cli.Delete(ctx, item); err != nil { + logger.Error(err, "failed to delete image version", "image version", item) + return fmt.Errorf("failed to delete image version: %w", err) + } + continue + } + if _, ok := item.Labels[config.LatestKey]; ok && item.Name != latest.Name { + delete(item.Labels, config.LatestKey) + if err := CreateOrUpdateImageVersion(ctx, cli, item); err != nil { + logger.Error(err, "failed to patch image version", "image version", item) + return fmt.Errorf("failed to patch image version: %w", err) + } + } + } + } + if latest.Labels[config.LatestKey] != "true" { + if latest.Labels == nil { + latest.Labels = map[string]string{} + } + latest.Labels[config.LatestKey] = "true" + return CreateOrUpdateImageVersion(ctx, cli, latest) + } + return nil +} + +func mergeImageVersion(oldIV, newIV *vc.ImageVersion) *vc.ImageVersion { + for name, oldComp := range oldIV.Spec.Components { + if comp, ok := newIV.Spec.Components[name]; ok { + for _, version := range oldComp.ComponentVersions { + if !slices.ContainsFunc(comp.ComponentVersions, func(vc vc.ComponentVersion) bool { + return vc.Tag == version.Tag + }) { + comp.ComponentVersions = append(comp.ComponentVersions, version) + } + } + newIV.Spec.Components[name] = comp + } else { + newIV.Spec.Components[name] = oldComp + } + } + return newIV +} + +func InstallOldImageVersion(ctx context.Context, cli client.Client, namespace string, logger logr.Logger) error { + iv, err := GenerateOldImageVersion(ctx, cli, namespace, logger) + if err != nil { + if stderrs.Is(err, ErrBundleVersionNotFound) { + return nil + } + return err + } + if iv == nil { + return nil + } + + logger.Info("install old image version", "version", iv.Name) + var oldIV vc.ImageVersion + if err := cli.Get(ctx, client.ObjectKeyFromObject(iv), &oldIV); errors.IsNotFound(err) { + return CreateOrUpdateImageVersion(ctx, cli, iv) + } else if err != nil { + return err + } + iv = mergeImageVersion(&oldIV, iv) + return CreateOrUpdateImageVersion(ctx, cli, iv) +} + +func InstallCurrentImageVersion(ctx context.Context, cli client.Client, logger logr.Logger) error { + iv, err := GenerateCurrentImageVersion(ctx, cli, logger) + if err != nil { + return err + } + return CreateOrUpdateImageVersion(ctx, cli, iv) +} diff --git a/internal/vc/generator_test.go b/internal/vc/generator_test.go new file mode 100644 index 0000000..a2eae51 --- /dev/null +++ b/internal/vc/generator_test.go @@ -0,0 +1,235 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vc + +import ( + "reflect" + "testing" + + "github.com/alauda/redis-operator/internal/config" + vc "github.com/alauda/redis-operator/internal/vc/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_mergeImageVersion(t *testing.T) { + version := "1.0.0" + type args struct { + oldIV *vc.ImageVersion + newIV *vc.ImageVersion + } + tests := []struct { + name string + args args + want *vc.ImageVersion + }{ + { + name: "merge image version", + args: args{ + oldIV: &vc.ImageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + config.InstanceTypeKey: config.CoreComponentName, + config.CRVersionKey: version, + }, + Name: GenerateRedisBundleImageVersion(version), + }, + Spec: vc.ImageVersionSpec{ + CrVersion: version, + Components: map[string]vc.Component{ + "redis": { + CoreComponent: true, + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "4.0-alpine.b2d19531", + Version: "4.0.0", + DisplayVersion: "4.0", + }, + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "5.0-alpine.b2d19531", + Version: "5.0.0", + DisplayVersion: "5.0", + }, + }, + }, + "redis-exporter": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/oliver006/redis_exporter", + Tag: "v1.3.5-bb5bec2c", + }, + }, + }, + "redis-tools": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis-tools", + Tag: "v3.14.7", + }, + }, + }, + "expose-pod": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/expose-pod", + Tag: "v3.14.50", + }, + }, + }, + }, + }, + }, + newIV: &vc.ImageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + config.InstanceTypeKey: config.CoreComponentName, + config.CRVersionKey: version, + }, + Name: GenerateRedisBundleImageVersion(version), + }, + Spec: vc.ImageVersionSpec{ + CrVersion: version, + Components: map[string]vc.Component{ + "redis": { + CoreComponent: true, + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "5.0-alpine.b2d19531", + Version: "5.0.0", + DisplayVersion: "5.0", + }, + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "6.0-alpine.b2d19531", + Version: "6.0.0", + DisplayVersion: "6.0", + }, + }, + }, + "activeredis": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "6.0-alpine.b2d19531", + Version: "6.0.0", + DisplayVersion: "6.0", + }, + }, + }, + "redis-exporter": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/oliver006/redis_exporter", + Tag: "v1.3.5-bb5bec2c", + }, + }, + }, + "redis-tools": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis-tools", + Tag: "v3.14.7", + }, + }, + }, + }, + }, + }, + }, + want: &vc.ImageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + config.InstanceTypeKey: config.CoreComponentName, + config.CRVersionKey: version, + }, + Name: GenerateRedisBundleImageVersion(version), + }, + Spec: vc.ImageVersionSpec{ + CrVersion: version, + Components: map[string]vc.Component{ + "redis": { + CoreComponent: true, + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "5.0-alpine.b2d19531", + Version: "5.0.0", + DisplayVersion: "5.0", + }, + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "6.0-alpine.b2d19531", + Version: "6.0.0", + DisplayVersion: "6.0", + }, + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "4.0-alpine.b2d19531", + Version: "4.0.0", + DisplayVersion: "4.0", + }, + }, + }, + "activeredis": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis", + Tag: "6.0-alpine.b2d19531", + Version: "6.0.0", + DisplayVersion: "6.0", + }, + }, + }, + "redis-exporter": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/oliver006/redis_exporter", + Tag: "v1.3.5-bb5bec2c", + }, + }, + }, + "redis-tools": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/redis-tools", + Tag: "v3.14.7", + }, + }, + }, + "expose-pod": { + ComponentVersions: []vc.ComponentVersion{ + { + Image: "build-harbor.alauda.cn/middleware/expose-pod", + Tag: "v3.14.50", + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := mergeImageVersion(tt.args.oldIV, tt.args.newIV); !reflect.DeepEqual(got, tt.want) { + t.Errorf("mergeImageVersion() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/vc/helper.go b/internal/vc/helper.go new file mode 100644 index 0000000..ca52809 --- /dev/null +++ b/internal/vc/helper.go @@ -0,0 +1,216 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vc + +import ( + "context" + "errors" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/internal/config" + v1 "github.com/alauda/redis-operator/internal/vc/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + ErrBundleVersionNotFound = errors.New("bundle version not found") + ErrInvalidImage = errors.New("invalid source image") +) + +type BundleVersion v1.ImageVersion + +func (bv *BundleVersion) IsLatest() bool { + return bv.Labels[config.LatestKey] == "true" +} + +func (bv *BundleVersion) GetComponent(compName string, displayVersion string) *v1.ComponentVersion { + if bv == nil || compName == "" { + return nil + } + + for name, comp := range bv.Spec.Components { + if name == compName { + for _, version := range comp.ComponentVersions { + if version.DisplayVersion == displayVersion || displayVersion == "" { + return version.DeepCopy() + } + } + } + } + return nil +} + +func (bv *BundleVersion) GetImage(compName string, displayVersion string) (string, error) { + if bv == nil || compName == "" { + return "", errors.New("image version is nil") + } + for name, comp := range bv.Spec.Components { + if name == compName { + for _, version := range comp.ComponentVersions { + if version.DisplayVersion == displayVersion || displayVersion == "" { + return fmt.Sprintf("%s:%s", version.Image, version.Tag), nil + } + } + } + } + return "", ErrBundleVersionNotFound +} + +func (bv *BundleVersion) GetRedisImage(displayVersion string) (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + return bv.GetImage("redis", displayVersion) +} + +func (bv *BundleVersion) GetDefaultRedisImage() (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + defaultVersion := config.DefaultRedisVersion + + return bv.GetRedisImage(defaultVersion) +} + +func (bv *BundleVersion) GetActiveRedisImage(version string) (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + return bv.GetImage("activeredis", version) +} + +func (bv *BundleVersion) GetRedisToolsImage() (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + return bv.GetImage("redis-tools", "") +} + +func (bv *BundleVersion) GetExposePodImage() (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + return bv.GetImage("expose-pod", "") +} + +func (bv *BundleVersion) GetRedisExporterImage() (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + return bv.GetImage("redis-exporter", "") +} + +func (bv *BundleVersion) GetRedisProxyImage() (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + return bv.GetImage("redis-proxy", "") +} + +func (bv *BundleVersion) GetRedisShakeImage() (string, error) { + if bv == nil { + return "", ErrBundleVersionNotFound + } + return bv.GetImage("redis-shake", "") +} + +func GetBundleVersion(ctx context.Context, cli client.Client, version string) (*BundleVersion, error) { + if version == "" { + return nil, nil + } + + if ver, _ := semver.NewVersion(version); ver != nil { + version = ver.String() + } + labels := map[string]string{ + config.InstanceTypeKey: config.CoreComponentName, + config.CRVersionKey: version, + } + + ret := v1.ImageVersionList{} + if err := cli.List(ctx, &ret, client.MatchingLabels(labels)); err != nil { + return nil, err + } + if len(ret.Items) > 1 { + return nil, fmt.Errorf("more than one image version for crVersion %s", version) + } + if len(ret.Items) == 0 { + return nil, ErrBundleVersionNotFound + } + return (*BundleVersion)(&ret.Items[0]), nil +} + +func GetLatestBundleVersion(ctx context.Context, cli client.Client) (*BundleVersion, error) { + labels := map[string]string{ + config.InstanceTypeKey: config.CoreComponentName, + config.LatestKey: "true", + } + + ret := v1.ImageVersionList{} + if err := cli.List(ctx, &ret, client.MatchingLabels(labels)); err != nil { + return nil, err + } + if len(ret.Items) == 0 { + return nil, ErrBundleVersionNotFound + } + return (*BundleVersion)(&ret.Items[0]), nil +} + +func IsBundleVersionUpgradeable(oldBV, newBV *BundleVersion, displayVersion string) bool { + if oldBV == nil { + return true + } + if newBV == nil { + return false + } + + compName := config.CoreComponentName + + oldVersion, err := semver.NewVersion(oldBV.Spec.CrVersion) + if err != nil { + return true + } + newVersion, err := semver.NewVersion(newBV.Spec.CrVersion) + if err != nil { + return false + } + if oldVersion.GreaterThan(newVersion) { + return false + } + + oldComp := oldBV.GetComponent(compName, displayVersion) + newComp := newBV.GetComponent(compName, displayVersion) + if oldComp == nil { + return true + } + if newComp == nil { + return false + } + if oldComp.Version == "" || newComp.Version == "" { + return true + } + oldCompVer, err := semver.NewVersion(oldComp.Version) + if err != nil { + return true + } + newCompVer, err := semver.NewVersion(newComp.Version) + if err != nil { + return false + } + return !oldCompVer.GreaterThan(newCompVer) +} diff --git a/pkg/types/node.go b/internal/vc/v1/groupversion_info.go similarity index 53% rename from pkg/types/node.go rename to internal/vc/v1/groupversion_info.go index d5217a5..46c5e81 100644 --- a/pkg/types/node.go +++ b/internal/vc/v1/groupversion_info.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,25 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package types +package v1 import ( - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" ) -type RedisBaseNode interface { - v1.Object - GetObjectKind() schema.ObjectKind - Definition() *corev1.Pod - Status() corev1.PodPhase -} +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "middleware.alauda.io", Version: "v1"} -type RedisNode interface { - RedisBaseNode -} + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} -type RedisSentinelNode interface { - RedisBaseNode -} + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/internal/vc/v1/imageversion_types.go b/internal/vc/v1/imageversion_types.go new file mode 100644 index 0000000..717c0b5 --- /dev/null +++ b/internal/vc/v1/imageversion_types.go @@ -0,0 +1,75 @@ +// +kubebuilder:skip + +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +kubebuilder:object:generate:=true + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ComponentVersion struct { + Image string `json:"image"` + Tag string `json:"tag"` + Version string `json:"version,omitempty"` + DisplayVersion string `json:"displayVersion,omitempty"` + Extensions map[string]Extensions `json:"extensions,omitempty"` +} + +type Extensions struct { + Version string `json:"version"` +} + +// ImageVersionSpec defines the desired state of ImageVersion +type ImageVersionSpec struct { + CrVersion string `json:"crVersion,omitempty"` + Components map[string]Component `json:"components,omitempty"` +} + +// ImageVersionStatus defines the observed state of ImageVersion +type ImageVersionStatus struct{} + +type Component struct { + CoreComponent bool `json:"coreComponent,omitempty"` + ComponentVersions []ComponentVersion `json:"versions"` +} + +// +kubebuilder:object:root=true + +// ImageVersion is the Schema for the imageversions API +type ImageVersion struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ImageVersionSpec `json:"spec,omitempty"` + Status ImageVersionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ImageVersionList contains a list of ImageVersion +type ImageVersionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ImageVersion `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ImageVersion{}, &ImageVersionList{}) +} diff --git a/internal/vc/v1/zz_generated.deepcopy.go b/internal/vc/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000..e6a85e2 --- /dev/null +++ b/internal/vc/v1/zz_generated.deepcopy.go @@ -0,0 +1,180 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Component) DeepCopyInto(out *Component) { + *out = *in + if in.ComponentVersions != nil { + in, out := &in.ComponentVersions, &out.ComponentVersions + *out = make([]ComponentVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Component. +func (in *Component) DeepCopy() *Component { + if in == nil { + return nil + } + out := new(Component) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentVersion) DeepCopyInto(out *ComponentVersion) { + *out = *in + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make(map[string]Extensions, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentVersion. +func (in *ComponentVersion) DeepCopy() *ComponentVersion { + if in == nil { + return nil + } + out := new(ComponentVersion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Extensions) DeepCopyInto(out *Extensions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extensions. +func (in *Extensions) DeepCopy() *Extensions { + if in == nil { + return nil + } + out := new(Extensions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageVersion) DeepCopyInto(out *ImageVersion) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageVersion. +func (in *ImageVersion) DeepCopy() *ImageVersion { + if in == nil { + return nil + } + out := new(ImageVersion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ImageVersion) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageVersionList) DeepCopyInto(out *ImageVersionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ImageVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageVersionList. +func (in *ImageVersionList) DeepCopy() *ImageVersionList { + if in == nil { + return nil + } + out := new(ImageVersionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ImageVersionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageVersionSpec) DeepCopyInto(out *ImageVersionSpec) { + *out = *in + if in.Components != nil { + in, out := &in.Components, &out.Components + *out = make(map[string]Component, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageVersionSpec. +func (in *ImageVersionSpec) DeepCopy() *ImageVersionSpec { + if in == nil { + return nil + } + out := new(ImageVersionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageVersionStatus) DeepCopyInto(out *ImageVersionStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageVersionStatus. +func (in *ImageVersionStatus) DeepCopy() *ImageVersionStatus { + if in == nil { + return nil + } + out := new(ImageVersionStatus) + in.DeepCopyInto(out) + return out +} diff --git a/observability/grafana/dashboards/redis_instance.json b/observability/grafana/dashboards/redis_instance.json new file mode 100644 index 0000000..6445a74 --- /dev/null +++ b/observability/grafana/dashboards/redis_instance.json @@ -0,0 +1,3145 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "OwnerReference: operators/redis", + "editable": false, + "gnetId": null, + "graphTooltip": 0, + "id": 3, + "iteration": 1664350000216, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 66, + "panels": [], + "title": "ClusterInfo", + "type": "row" + }, + { + "cacheTimeout": null, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 13, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" + ], + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.0.0", + "repeat": null, + "repeatDirection": "h", + "targets": [ + { + "exemplar": true, + "expr": "max(redis_connected_clients{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "Connected Clients", + "type": "stat" + }, + { + "cacheTimeout": null, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurations" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 15, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "7.0.0", + "targets": [ + { + "exemplar": true, + "expr": "max(redis_uptime_in_seconds{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Uptime", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "ops" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 6 + }, + "hiddenSeries": false, + "id": 12, + "interval": null, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(irate(redis_commands_processed_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[3m])) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Commands", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 12 + }, + "hiddenSeries": false, + "id": 45, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "application": { + "filter": "" + }, + "exemplar": true, + "expr": "max(irate(redis_commands_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\"}[3m])) by (cmd, service)", + "format": "time_series", + "functions": [], + "group": { + "filter": "" + }, + "host": { + "filter": "" + }, + "interval": "", + "intervalFactor": 1, + "item": { + "filter": "" + }, + "legendFormat": "{{cmd}}", + "mode": 0, + "options": { + "showDisabledItems": false, + "skipEmptyValues": false + }, + "refId": "A", + "resultFormat": "time_series", + "table": { + "skipEmptyValues": false + }, + "triggers": { + "acknowledged": 2, + "count": true, + "minSeverity": 3 + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Command calls", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 19 + }, + "hiddenSeries": false, + "id": 14, + "interval": null, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(redis_db_keys{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (db,pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}#{{db}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Number of Keys", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 24 + }, + "hiddenSeries": false, + "id": 86, + "interval": null, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(redis_pubsub_channels{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "PubSubChannels", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 29 + }, + "hiddenSeries": false, + "id": 87, + "interval": null, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(redis_pubsub_patterns{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "PubSubPatterns", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 34 + }, + "hiddenSeries": false, + "id": 44, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "application": { + "filter": "" + }, + "exemplar": true, + "expr": "sum(rate(redis_slowlog_length{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[3m])) by (pod)", + "format": "time_series", + "functions": [], + "group": { + "filter": "" + }, + "host": { + "filter": "" + }, + "interval": "", + "intervalFactor": 1, + "item": { + "filter": "" + }, + "legendFormat": "{{pod}}", + "mode": 0, + "options": { + "showDisabledItems": false, + "skipEmptyValues": false + }, + "refId": "A", + "resultFormat": "time_series", + "table": { + "skipEmptyValues": false + }, + "triggers": { + "acknowledged": 2, + "count": true, + "minSeverity": 3 + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Slow log", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 40 + }, + "hiddenSeries": false, + "id": 50, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "application": { + "filter": "" + }, + "exemplar": true, + "expr": "max(irate(redis_evicted_keys_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[3m])) by (pod)", + "format": "time_series", + "functions": [], + "group": { + "filter": "" + }, + "host": { + "filter": "" + }, + "interval": "", + "intervalFactor": 1, + "item": { + "filter": "" + }, + "legendFormat": "{{pod}}(Evicted)", + "mode": 0, + "options": { + "showDisabledItems": false, + "skipEmptyValues": false + }, + "refId": "A", + "resultFormat": "time_series", + "table": { + "skipEmptyValues": false + }, + "triggers": { + "acknowledged": 2, + "count": true, + "minSeverity": 3 + } + }, + { + "exemplar": true, + "expr": "max(irate(redis_expired_keys_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[3m])) by (pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}(Expired)", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Keys Statistics", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "ops" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 50 + }, + "hiddenSeries": false, + "id": 42, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(irate(redis_keyspace_hits_total{vmcluster=\"$vmcluster\",service=~\"$Instance\", namespace=\"$Namespace\"}[5m])) by (pod)", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Keyspace Hit", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 10, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 10, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "ops" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 56 + }, + "hiddenSeries": false, + "id": 90, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(irate(redis_keyspace_misses_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\"}[5m])) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": " {{pod}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Keyspace Misses", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 10, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 10, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "decimals": 0, + "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 62 + }, + "hiddenSeries": false, + "id": 56, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "application": { + "filter": "" + }, + "exemplar": true, + "expr": "max(redis_blocked_clients{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod) ", + "format": "time_series", + "functions": [], + "group": { + "filter": "" + }, + "host": { + "filter": "" + }, + "interval": "", + "intervalFactor": 1, + "item": { + "filter": "" + }, + "legendFormat": "{{pod}}", + "mode": 0, + "options": { + "showDisabledItems": false, + "skipEmptyValues": false + }, + "refId": "A", + "resultFormat": "time_series", + "table": { + "skipEmptyValues": false + }, + "triggers": { + "acknowledged": 2, + "count": true, + "minSeverity": 3 + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "BlockedClients", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "columns": [], + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fontSize": "100%", + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 69 + }, + "id": 47, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 1, + "desc": false + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "hidden" + }, + { + "alias": "Duration per call", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/Value.*/", + "thresholds": [], + "type": "number", + "unit": "s" + }, + { + "alias": "Command", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "cmd", + "thresholds": [], + "type": "string", + "unit": "short" + }, + { + "alias": "", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "service", + "thresholds": [], + "type": "hidden", + "unit": "short" + } + ], + "targets": [ + { + "exemplar": true, + "expr": "avg(redis_commands_duration_seconds_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\"} / redis_commands_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\"}) by (cmd, service)", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "title": "Command statistics", + "transform": "table", + "type": "table-old" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 75 + }, + "id": 62, + "panels": [], + "title": "I/O", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 76 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "application": { + "filter": "" + }, + "exemplar": true, + "expr": "sum(rate(redis_net_input_bytes_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[5m])) by (pod)", + "format": "time_series", + "functions": [], + "group": { + "filter": "" + }, + "host": { + "filter": "" + }, + "interval": "", + "intervalFactor": 1, + "item": { + "filter": "" + }, + "legendFormat": "{{pod}}", + "mode": 0, + "options": { + "showDisabledItems": false, + "skipEmptyValues": false + }, + "refId": "A", + "resultFormat": "time_series", + "table": { + "skipEmptyValues": false + }, + "triggers": { + "acknowledged": 2, + "count": true, + "minSeverity": 3 + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network I/O (Input)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 76 + }, + "hiddenSeries": false, + "id": 64, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum(irate(redis_net_output_bytes_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[5m])) by (pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network I/O(Output)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "percent" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 84 + }, + "hiddenSeries": false, + "id": 88, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum(irate(redis_cpu_user_seconds_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[5m])*100) by (pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}(User)", + "refId": "B" + }, + { + "exemplar": true, + "expr": "sum(irate(redis_cpu_user_children_seconds_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[5m]) * 100) by (pod)", + "hide": false, + "interval": "", + "legendFormat": "{{pod}}(User[child])", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Cpu User", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "percent" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 84 + }, + "hiddenSeries": false, + "id": 89, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum(irate(redis_cpu_sys_seconds_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[5m])*100) by (pod)", + "hide": false, + "interval": "", + "legendFormat": "{{pod}}(Sys)", + "refId": "A" + }, + { + "exemplar": true, + "expr": "sum(irate(redis_cpu_sys_children_seconds_total{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}[5m])*100) by (pod)", + "hide": false, + "interval": "", + "legendFormat": "{{pod}}(Sys[child])", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Cpu Sys", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 92 + }, + "id": 60, + "panels": [], + "title": "Memory", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "decbytes" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 93 + }, + "hiddenSeries": false, + "id": 72, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(redis_memory_used_bytes{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "MemoryUsed", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "none" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 93 + }, + "hiddenSeries": false, + "id": 70, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "application": { + "filter": "" + }, + "exemplar": true, + "expr": "redis_memory_used_rss_bytes{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"} / redis_memory_used_bytes{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}", + "format": "time_series", + "functions": [], + "group": { + "filter": "" + }, + "host": { + "filter": "" + }, + "interval": "", + "intervalFactor": 1, + "item": { + "filter": "" + }, + "legendFormat": "{{pod}}", + "mode": 0, + "options": { + "showDisabledItems": false, + "skipEmptyValues": false + }, + "refId": "A", + "resultFormat": "time_series", + "table": { + "skipEmptyValues": false + }, + "triggers": { + "acknowledged": 2, + "count": true, + "minSeverity": 3 + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "MemFragmentationRatio", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "decbytes" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 101 + }, + "hiddenSeries": false, + "id": 74, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(redis_memory_used_peak_bytes{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "MemoryUsed,Peek", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [], + "unit": "decbytes" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 101 + }, + "hiddenSeries": false, + "id": 76, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.5.16", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "max(redis_memory_max_bytes{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "MemoryMax", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 109 + }, + "id": 78, + "panels": [], + "title": "Persistence", + "type": "row" + }, + { + "cacheTimeout": null, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 110 + }, + "id": 80, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "mean" + ], + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.0.0", + "repeat": null, + "repeatDirection": "h", + "targets": [ + { + "exemplar": true, + "expr": "max(redis_aof_rewrite_in_progress{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "AOFRewriteInProgress", + "type": "stat" + }, + { + "cacheTimeout": null, + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 110 + }, + "id": 82, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "mean" + ], + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.0.0", + "repeat": null, + "repeatDirection": "h", + "targets": [ + { + "exemplar": true, + "expr": "max(redis_rdb_bgsave_in_progress{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "RDBBgsaveInProgress", + "type": "stat" + }, + { + "columns": [], + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "filterable": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "LastSaveDuration" + } + ] + } + ] + }, + "fontSize": "100%", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 115 + }, + "id": 85, + "options": { + "frameIndex": 1, + "showHeader": true + }, + "pageSize": null, + "pluginVersion": "7.0.0", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "number" + }, + { + "alias": "", + "align": "right", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "exemplar": true, + "expr": "max(redis_aof_last_rewrite_duration_sec{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"} ) by (pod)", + "format": "time_series", + "instant": true, + "interval": "", + "intervalFactor": 3, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "AOFLastRewriteDuration", + "transform": "table", + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "table-old" + }, + { + "columns": [], + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "filterable": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dateTimeAsSystem" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "LastSaveTime" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "pod" + }, + "properties": [ + { + "id": "custom.width", + "value": 334 + } + ] + } + ] + }, + "fontSize": "100%", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 115 + }, + "id": 84, + "options": { + "frameIndex": 1, + "showHeader": true, + "sortBy": [] + }, + "pageSize": null, + "pluginVersion": "7.0.0", + "repeat": null, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Pod", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "pod", + "type": "string" + }, + { + "alias": "Date", + "align": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "Value", + "thresholds": [], + "type": "date", + "unit": "short" + } + ], + "targets": [ + { + "exemplar": true, + "expr": "max(redis_rdb_last_save_timestamp_seconds{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"} *1000 ) by (pod)", + "format": "time_series", + "instant": true, + "interval": "", + "intervalFactor": 3, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "RDBLastSaveTime", + "transform": "table", + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": false + }, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "table-old" + }, + { + "columns": [], + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "filterable": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "LastBgsaveDuration" + } + ] + } + ] + }, + "fontSize": "100%", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 123 + }, + "id": 92, + "options": { + "frameIndex": 1, + "showHeader": true + }, + "pageSize": null, + "pluginVersion": "7.0.0", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "number" + }, + { + "alias": "", + "align": "right", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "exemplar": true, + "expr": "max(redis_rdb_last_bgsave_duration_sec{vmcluster=\"$vmcluster\",service=\"$Instance\", namespace=\"$Namespace\",pod=~\"$Pod\"}) by (pod)", + "format": "time_series", + "instant": true, + "interval": "", + "intervalFactor": 3, + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "RDBLastBgsaveDuration", + "transform": "table", + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "table-old" + } + ], + "refresh": "15m", + "schemaVersion": 25, + "style": "dark", + "tags": [ + "Redis" + ], + "templating": { + "list": [ + { + "allFormat": "", + "allValue": "", + "current": { + "selected": false, + "text": "No data sources found", + "value": "" + }, + "datasource": null, + "description": null, + "error": null, + "hide": 0, + "includeAll": false, + "label": "", + "multi": false, + "multiFormat": "", + "name": "Datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "datasource" + }, + { + "allFormat": "", + "allValue": "", + "current": { + "selected": false, + "value": "" + }, + "datasource": "prometheus", + "definition": "label_values(redis_instance_info{role=\"master\"}, vmcluster)", + "description": null, + "error": null, + "hide": 0, + "includeAll": false, + "label": "Kubernetes Cluster", + "multi": false, + "multiFormat": "", + "name": "vmcluster", + "options": [], + "query": { + "query": "label_values(redis_instance_info{role=\"master\"}, vmcluster)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "", + "allValue": "", + "current": { + "selected": false, + "value": "hfxia-dev" + }, + "datasource": "prometheus", + "definition": "", + "description": null, + "error": null, + "hide": 0, + "includeAll": false, + "label": "", + "multi": false, + "multiFormat": "", + "name": "Namespace", + "options": [], + "query": { + "query": "label_values(redis_instance_info{vmcluster=\"$vmcluster\",role=\"master\"}, namespace)", + "refId": "prometheus-Namespace-Variable-Query" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "", + "allValue": "", + "current": { + "selected": false + }, + "datasource": "prometheus", + "definition": "", + "description": null, + "error": null, + "hide": 0, + "includeAll": false, + "label": "", + "multi": false, + "multiFormat": "", + "name": "Instance", + "options": [], + "query": { + "query": "label_values(redis_instance_info{vmcluster=\"$vmcluster\",role=\"master\", namespace=\"$Namespace\"}, service)", + "refId": "prometheus-Instance-Variable-Query" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "", + "allValue": "", + "current": { + "selected": false, + "value": "$__all" + }, + "datasource": "prometheus", + "definition": "", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": "", + "multi": false, + "multiFormat": "", + "name": "Role", + "options": [], + "query": { + "query": "label_values(redis_instance_info{namespace=\"$Namespace\",vmcluster=\"$vmcluster\",service=~\"$Instance\"}, role)", + "refId": "prometheus-Role-Variable-Query" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "", + "allValue": "", + "current": { + "selected": true, + "value": [ + "$__all" + ] + }, + "datasource": "prometheus", + "definition": "", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": "", + "multi": true, + "multiFormat": "", + "name": "Pod", + "options": [], + "query": { + "query": "label_values(redis_instance_info{namespace=\"$Namespace\",vmcluster=\"$vmcluster\",service=~\"$Instance\",role=~\"$Role\"}, pod)", + "refId": "prometheus-Pod-Variable-Query" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-7d", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Redis Dashboard", + "uid": "redis-overview", + "version": 7 +} diff --git a/pkg/actor/actor.go b/pkg/actor/actor.go index 8907f8a..c6029ac 100644 --- a/pkg/actor/actor.go +++ b/pkg/actor/actor.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,13 +19,10 @@ package actor import ( "context" + "github.com/Masterminds/semver/v3" "github.com/alauda/redis-operator/pkg/types" ) -type Command interface { - String() string -} - // ActorResult type ActorResult struct { next Command @@ -44,6 +41,22 @@ func NewResultWithError(cmd Command, err error) *ActorResult { return &ActorResult{next: cmd, result: err} } +func Requeue() *ActorResult { + return &ActorResult{next: CommandRequeue} +} + +func RequeueWithError(err error) *ActorResult { + return &ActorResult{next: CommandRequeue, result: err} +} + +func Pause() *ActorResult { + return &ActorResult{next: CommandPaused} +} + +func AbortWithError(err error) *ActorResult { + return &ActorResult{next: CommandAbort, result: err} +} + // Next func (c *ActorResult) NextCommand() Command { if c == nil { @@ -73,6 +86,11 @@ func (c *ActorResult) Err() error { // Actor actor is used process instance with specified state type Actor interface { + // SupportedCommands return the supported commands of the actor SupportedCommands() []Command + // Version return the version of the actor + // if the version is different from the previous version, the actor will be reloaded + Version() *semver.Version + // Do run the actor Do(ctx context.Context, cluster types.RedisInstance) *ActorResult } diff --git a/pkg/actor/actor_test.go b/pkg/actor/actor_test.go new file mode 100644 index 0000000..ed414b0 --- /dev/null +++ b/pkg/actor/actor_test.go @@ -0,0 +1,165 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "errors" + "reflect" + "testing" + "time" +) + +func TestDefinedCommands(t *testing.T) { + if (*ActorResult)(nil).NextCommand() != nil { + t.Errorf("ActorResult.NextCommand() = %v, want %v", (*ActorResult)(nil).NextCommand(), nil) + } + if Requeue().NextCommand() != CommandRequeue { + t.Errorf("Requeue() = %v, want %v", Requeue().NextCommand(), CommandRequeue) + } + + if RequeueWithError(errors.New("test")).NextCommand() != CommandRequeue { + t.Errorf("RequeueWithError() = %v, want %v", RequeueWithError(errors.New("test")).NextCommand(), CommandRequeue) + } + + if Pause().NextCommand() != CommandPaused { + t.Errorf("Pause() = %v, want %v", Pause().NextCommand(), CommandPaused) + } + if AbortWithError(errors.New("test")).NextCommand() != CommandAbort { + t.Errorf("AbortWithError() = %v, want %v", AbortWithError(errors.New("test")).NextCommand(), CommandAbort) + } +} + +func TestNewResult(t *testing.T) { + type args struct { + cmd Command + } + tests := []struct { + name string + args args + want *ActorResult + }{ + { + name: "nil command", + args: args{}, + want: &ActorResult{}, + }, + { + name: "TestNewResult", + args: args{ + cmd: CmdFailoverEnsureResource, + }, + want: &ActorResult{ + next: CmdFailoverEnsureResource, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewResult(tt.args.cmd); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewResult() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewResultWithValue(t *testing.T) { + type args struct { + cmd Command + val interface{} + } + tests := []struct { + name string + args args + want *ActorResult + }{ + { + name: "TestNewResultWithValue", + args: args{ + cmd: CommandRequeue, + val: time.Second, + }, + want: &ActorResult{ + next: CommandRequeue, + result: time.Second, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewResultWithValue(tt.args.cmd, tt.args.val) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewResultWithValue() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got.Result(), tt.args.val) { + t.Errorf("NewResultWithValue() = %v, want %v", got.Result(), tt.args.val) + } + if got.Err() != nil { + t.Errorf("NewResultWithValue() = %v, want %v", got.Err(), nil) + } + }) + } + if (*ActorResult)(nil).Result() != nil { + t.Errorf("ActorResult.Result() = %v, want %v", (&ActorResult{}).Result(), nil) + } +} + +func TestNewResultWithError(t *testing.T) { + type args struct { + cmd Command + err error + } + err := errors.New("invalid resource") + tests := []struct { + name string + args args + want *ActorResult + }{ + { + name: "nil error", + args: args{ + cmd: CommandRequeue, + err: nil, + }, + want: &ActorResult{ + next: CommandRequeue, + result: nil, + }, + }, + { + name: "TestNewResultWithError", + args: args{ + cmd: CommandRequeue, + err: err, + }, + want: &ActorResult{ + next: CommandRequeue, + result: err, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewResultWithError(tt.args.cmd, tt.args.err) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewResultWithError() = %v, want %v", got, tt.want) + } + if got.Err() != tt.args.err { + t.Errorf("NewResultWithError() = %v, want %v", got.Err(), tt.args.err) + } + }) + } +} diff --git a/pkg/actor/command.go b/pkg/actor/command.go new file mode 100644 index 0000000..e182bc3 --- /dev/null +++ b/pkg/actor/command.go @@ -0,0 +1,48 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "github.com/alauda/redis-operator/api/core" +) + +type Command interface { + String() string +} + +type opsCommand struct { + // arch is the architecture of the command, empty means all architectures + arch core.Arch + command string +} + +func (c *opsCommand) String() string { + return c.command +} + +func NewCommand(arch core.Arch, command string) Command { + return &opsCommand{ + arch: arch, + command: command, + } +} + +var ( + CommandRequeue = NewCommand("", "CommandRequeue") + CommandAbort = NewCommand("", "CommandAbort") + CommandPaused = NewCommand("", "CommandPaused") +) diff --git a/pkg/actor/command_test.go b/pkg/actor/command_test.go new file mode 100644 index 0000000..9b1c17b --- /dev/null +++ b/pkg/actor/command_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "reflect" + "testing" + + "github.com/alauda/redis-operator/api/core" +) + +func Test_opsCommand_String(t *testing.T) { + type fields struct { + arch core.Arch + command string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "string", + fields: fields{ + arch: core.RedisCluster, + command: "CommandEnsureResource", + }, + want: "CommandEnsureResource", + }, + { + name: "without arch", + fields: fields{ + command: "CommandEnsureResource", + }, + want: "CommandEnsureResource", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &opsCommand{ + arch: tt.fields.arch, + command: tt.fields.command, + } + if got := c.String(); got != tt.want { + t.Errorf("opsCommand.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewCommand(t *testing.T) { + type args struct { + arch core.Arch + command string + } + tests := []struct { + name string + args args + want Command + }{ + { + name: "new command", + args: args{ + arch: core.RedisCluster, + command: "CommandEnsureResource", + }, + want: &opsCommand{arch: core.RedisCluster, command: "CommandEnsureResource"}, + }, + { + name: "without arch command", + args: args{ + command: "CommandRequeue", + }, + want: &opsCommand{command: "CommandRequeue"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewCommand(tt.args.arch, tt.args.command); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewCommand() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/actor/manager.go b/pkg/actor/manager.go index 2e09fcf..e333b17 100644 --- a/pkg/actor/manager.go +++ b/pkg/actor/manager.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -16,44 +16,144 @@ limitations under the License. package actor +import ( + "sort" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/go-logr/logr" + "github.com/samber/lo" +) + +var ( + registeredActorInitializer = map[core.Arch][]func(kubernetes.ClientSet, logr.Logger) Actor{} +) + +func Register(a core.Arch, f func(kubernetes.ClientSet, logr.Logger) Actor) { + if _, ok := registeredActorInitializer[a]; !ok { + registeredActorInitializer[a] = []func(kubernetes.ClientSet, logr.Logger) Actor{} + } + registeredActorInitializer[a] = append(registeredActorInitializer[a], f) +} + +type VersionedActor []Actor + +type ActorGroup map[string]VersionedActor + +func (ag ActorGroup) Add(a Actor) { + for _, cmd := range a.SupportedCommands() { + ag[cmd.String()] = append(ag[cmd.String()], a) + } +} + +func (ag ActorGroup) Get(cmd Command) []Actor { + if actors, ok := ag[cmd.String()]; ok { + return actors + } + return nil +} + +func (ag ActorGroup) All() (ret [][]Actor) { + for _, actors := range ag { + ret = append(ret, actors) + } + return +} + // ActorManager type ActorManager struct { - actors map[string][]Actor + actors map[core.Arch]ActorGroup + logger logr.Logger } // NewActorManager -func NewActorManager() *ActorManager { - return &ActorManager{ - actors: map[string][]Actor{}, +func NewActorManager(cs kubernetes.ClientSet, logger logr.Logger) *ActorManager { + m := &ActorManager{ + actors: map[core.Arch]ActorGroup{}, + logger: logger, } + for arch, inits := range registeredActorInitializer { + for _, init := range inits { + m.Add(arch, init(cs, logger)) + } + } + return m } -// Get -func (m *ActorManager) Get(cmd Command) []Actor { +func (m *ActorManager) Print() { if m == nil { - return nil + return } - if actors, ok := m.actors[cmd.String()]; ok { - return actors + + for arch, ag := range m.actors { + cmds := map[string][]string{} + for cmd, actors := range ag { + versions := lo.Map(actors, func(actor Actor, index int) string { + return actor.Version().String() + }) + cmds[cmd] = versions + } + m.logger.Info("ActorManager", "arch", string(arch), "commands", cmds) } - return nil } -// GetAll -func (m *ActorManager) All(cmd Command) (ret [][]Actor) { +type Object interface { + GetAnnotations() map[string]string + Arch() core.Arch +} + +// Search find the actor which match the version requirement (version >= actor.Version) +func (m *ActorManager) Search(cmd Command, inst Object) Actor { if m == nil { return nil } - for _, actors := range m.actors { - ret = append(ret, actors) + + crVersion := inst.GetAnnotations()[config.CRVersionKey] + if crVersion == "" { + crVersion = config.GetOperatorVersion() } - return + ver, _ := semver.NewVersion(crVersion) + if ver == nil { + return nil + } + if val, err := ver.SetPrerelease(""); err == nil { + ver = &val + } + if val, err := ver.SetMetadata(""); err == nil { + ver = &val + } + + if ag := m.actors[core.Arch(inst.Arch())]; ag != nil { + actors := ag.Get(cmd) + if len(actors) == 0 { + return nil + } else if len(actors) == 1 { + return actors[0] + } + sort.SliceStable(actors, func(i, j int) bool { + return actors[i].Version().GreaterThan(actors[j].Version()) + }) + for _, actor := range actors { + if !ver.LessThan(actor.Version()) { + return actor + } + } + } + return nil } // Add -func (m *ActorManager) Add(a Actor) error { - for _, cmd := range a.SupportedCommands() { - m.actors[cmd.String()] = append(m.actors[cmd.String()], a) +func (m *ActorManager) Add(arch core.Arch, a Actor) { + if m == nil { + return } - return nil + + ag := m.actors[arch] + if ag == nil { + m.actors[arch] = ActorGroup{} + ag = m.actors[arch] + } + ag.Add(a) } diff --git a/pkg/actor/manager_test.go b/pkg/actor/manager_test.go new file mode 100644 index 0000000..09f50a5 --- /dev/null +++ b/pkg/actor/manager_test.go @@ -0,0 +1,370 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package actor + +import ( + "context" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" + "github.com/alauda/redis-operator/internal/config" + "github.com/alauda/redis-operator/pkg/kubernetes" + "github.com/alauda/redis-operator/pkg/types" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var ( + logger = logr.New(nil) +) + +// Mock dependencies +type MockClientSet struct { + mock.Mock +} + +var ( + CmdClustetrEnsureResource = NewCommand(core.RedisCluster, "CommandEnsureResource") + CmdClustetrHealPod = NewCommand(core.RedisCluster, "CommandHealPod") + CmdFailoverEnsureResource = NewCommand(core.RedisSentinel, "CommandEnsureResource") + CmdFailoverHealPod = NewCommand(core.RedisSentinel, "CommandHealPod") +) + +type MockClusterEnsureResource struct { + mock.Mock +} + +func MockNewClusterEnsureResource(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockClusterEnsureResource{} +} + +func (m *MockClusterEnsureResource) SupportedCommands() []Command { + return []Command{CmdClustetrEnsureResource} +} + +func (m *MockClusterEnsureResource) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +func (m *MockClusterEnsureResource) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockClusterHealPodActor struct { + mock.Mock +} + +func MockNewClusterHealPodActor(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockClusterHealPodActor{} +} + +func (m *MockClusterHealPodActor) SupportedCommands() []Command { + return []Command{CmdClustetrHealPod} +} + +func (m *MockClusterHealPodActor) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +func (m *MockClusterHealPodActor) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockClusterEnsureResource314 struct { + mock.Mock +} + +func MockNewClusterEnsureResource314(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockClusterEnsureResource314{} +} + +func (m *MockClusterEnsureResource314) SupportedCommands() []Command { + return []Command{CmdClustetrEnsureResource} +} + +func (m *MockClusterEnsureResource314) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + +func (m *MockClusterEnsureResource314) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockClusterHealPodActor314 struct { + mock.Mock +} + +func MockNewClusterHealPodActor314(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockClusterHealPodActor314{} +} + +func (m *MockClusterHealPodActor314) SupportedCommands() []Command { + return []Command{CmdClustetrHealPod} +} + +func (m *MockClusterHealPodActor314) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + +func (m *MockClusterHealPodActor314) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockFailoverEnsureResource struct { + mock.Mock +} + +func MockNewFailoverEnsureResource(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockFailoverEnsureResource{} +} + +func (m *MockFailoverEnsureResource) SupportedCommands() []Command { + return []Command{CmdFailoverEnsureResource} +} + +func (m *MockFailoverEnsureResource) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +func (m *MockFailoverEnsureResource) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockFailoverHealPodActor struct { + mock.Mock +} + +func MockNewFailoverHealPodActor(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockFailoverHealPodActor{} +} + +func (m *MockFailoverHealPodActor) SupportedCommands() []Command { + return []Command{CmdFailoverHealPod} +} + +func (m *MockFailoverHealPodActor) Version() *semver.Version { + return semver.MustParse("3.18.0") +} + +func (m *MockFailoverHealPodActor) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockFailoverEnsureResource314 struct { + mock.Mock +} + +func MockNewFailoverEnsureResource314(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockFailoverEnsureResource314{} +} + +func (m *MockFailoverEnsureResource314) SupportedCommands() []Command { + return []Command{CmdFailoverEnsureResource} +} + +func (m *MockFailoverEnsureResource314) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + +func (m *MockFailoverEnsureResource314) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockFailoverHealPodActor314 struct { + mock.Mock +} + +func MockNewFailoverHealPodActor314(cs kubernetes.ClientSet, logger logr.Logger) Actor { + return &MockFailoverHealPodActor314{} +} + +func (m *MockFailoverHealPodActor314) SupportedCommands() []Command { + return []Command{CmdFailoverHealPod} +} + +func (m *MockFailoverHealPodActor314) Version() *semver.Version { + return semver.MustParse("3.14.0") +} + +func (m *MockFailoverHealPodActor314) Do(ctx context.Context, inst types.RedisInstance) *ActorResult { + args := m.Called() + return args.Get(0).(*ActorResult) +} + +type MockObject struct { + annotations map[string]string + arch core.Arch +} + +func (m *MockObject) GetAnnotations() map[string]string { + return m.annotations +} + +func (m *MockObject) Arch() core.Arch { + return m.arch +} + +func init() { + Register(core.RedisCluster, MockNewClusterEnsureResource) + Register(core.RedisCluster, MockNewClusterHealPodActor) + Register(core.RedisSentinel, MockNewFailoverEnsureResource) + Register(core.RedisSentinel, MockNewFailoverHealPodActor) + + Register(core.RedisCluster, MockNewClusterEnsureResource314) + Register(core.RedisCluster, MockNewClusterHealPodActor314) + Register(core.RedisSentinel, MockNewFailoverEnsureResource314) + Register(core.RedisSentinel, MockNewFailoverHealPodActor314) +} + +func TestRegister(t *testing.T) { + assert.NotNil(t, registeredActorInitializer[core.RedisCluster]) + assert.Equal(t, 4, len(registeredActorInitializer[core.RedisCluster])) + assert.NotNil(t, registeredActorInitializer[core.RedisSentinel]) + assert.Equal(t, 4, len(registeredActorInitializer[core.RedisSentinel])) +} + +func TestNewActorManager(t *testing.T) { + am := NewActorManager(nil, logger) + assert.NotNil(t, am) + assert.NotNil(t, am.actors[core.RedisCluster]) + assert.NotNil(t, am.actors[core.RedisSentinel]) + assert.Nil(t, am.actors[core.RedisStdSentinel]) +} + +func TestActorManager_Print(t *testing.T) { + am := NewActorManager(nil, logger) + am.Print() + for arch, ag := range am.actors { + if len(ag.All()) == 0 { + t.Errorf("arch %s has no actors", arch) + } + } +} + +func TestActorManager_Search(t *testing.T) { + am := NewActorManager(nil, logger) + for _, ver := range []string{"3.18.0", "3.18.10", "3.18.10-11111", "3.18.1-1111-bbbb"} { + inst := &MockObject{ + annotations: map[string]string{config.CRVersionKey: ver}, + arch: core.RedisCluster, + } + + { + cmd := &MockClusterEnsureResource{} + foundActor := am.Search(CmdClustetrEnsureResource, inst) + assert.NotNil(t, foundActor) + assert.Equal(t, cmd.Version().String(), foundActor.Version().String()) + } + + { + cmd := MockClusterHealPodActor{} + foundActor := am.Search(CmdClustetrHealPod, inst) + assert.NotNil(t, foundActor) + assert.Equal(t, cmd.Version().String(), foundActor.Version().String()) + } + } + + for _, ver := range []string{ + "3.14.0", "3.14.10", "3.14.10-11111", "3.14.1-1111-bbbb", + "3.15.0", "3.15.10", "3.15.10-11111", "3.15.1-1111-bbbb", + "3.16.0", "3.16.10", "3.16.10-11111", "3.16.1-1111-bbbb", + "3.17.0", "3.17.10", "3.17.10-11111", "3.17.1-1111-bbbb", + } { + inst := &MockObject{ + annotations: map[string]string{config.CRVersionKey: ver}, + arch: core.RedisCluster, + } + + { + t.Logf("version %s", ver) + cmd := &MockClusterEnsureResource314{} + foundActor := am.Search(CmdClustetrEnsureResource, inst) + assert.NotNil(t, foundActor) + assert.Equal(t, cmd.Version().String(), foundActor.Version().String()) + } + + { + cmd := &MockClusterHealPodActor314{} + foundActor := am.Search(CmdClustetrHealPod, inst) + assert.NotNil(t, foundActor) + assert.Equal(t, cmd.Version().String(), foundActor.Version().String()) + } + } + + for _, ver := range []string{ + "3.14.0", "3.14.10", "3.14.10-11111", "3.14.1-1111-bbbb", + "3.15.0", "3.15.10", "3.15.10-11111", "3.15.1-1111-bbbb", + "3.16.0", "3.16.10", "3.16.10-11111", "3.16.1-1111-bbbb", + "3.17.0", "3.17.10", "3.17.10-11111", "3.17.1-1111-bbbb", + } { + inst := &MockObject{ + annotations: map[string]string{config.CRVersionKey: ver}, + arch: core.RedisSentinel, + } + + { + t.Logf("version %s", ver) + cmd := &MockFailoverEnsureResource314{} + foundActor := am.Search(CmdFailoverEnsureResource, inst) + assert.NotNil(t, foundActor) + assert.Equal(t, cmd.Version().String(), foundActor.Version().String()) + } + + { + cmd := &MockFailoverHealPodActor314{} + foundActor := am.Search(CmdFailoverHealPod, inst) + assert.NotNil(t, foundActor) + assert.Equal(t, cmd.Version().String(), foundActor.Version().String()) + } + } + + { + inst := &MockObject{ + annotations: map[string]string{config.CRVersionKey: "3.18.0"}, + } + + { + foundActor := am.Search(CmdFailoverHealPod, inst) + assert.Nil(t, foundActor) + } + } + + inst := &MockObject{ + annotations: map[string]string{}, + arch: core.RedisSentinel, + } + + foundActor := am.Search(CmdFailoverHealPod, inst) + assert.Nil(t, foundActor) + + am = nil + am.Print() + if am.Search(CmdClustetrHealPod, inst) != nil { + t.Errorf("Search should return nil") + } + am.Add(core.RedisCluster, MockNewClusterEnsureResource(nil, logger)) +} diff --git a/pkg/config/keys.go b/pkg/config/keys.go deleted file mode 100644 index 01ed70d..0000000 --- a/pkg/config/keys.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package config - -const ( - RedisSecretUsernameKey = "username" - RedisSecretPasswordKey = "password" - S3_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID" - S3_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY" - S3_TOKEN = "TOKEN" - S3_REGION = "REGION" - S3_ENDPOINTURL = "ENDPOINTURL" - PAUSE_ANNOTATION_KEY = "app.cpaas.io/pause-timestamp" -) diff --git a/pkg/kubernetes/builder/clusterbuilder/cronjob.go b/pkg/kubernetes/builder/clusterbuilder/cronjob.go deleted file mode 100644 index a7a7ec2..0000000 --- a/pkg/kubernetes/builder/clusterbuilder/cronjob.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package clusterbuilder - -import ( - "reflect" - - "github.com/go-logr/logr" - batchv1 "k8s.io/api/batch/v1" -) - -func IsCronJobChanged(newJob, oldJob *batchv1.CronJob, logger logr.Logger) bool { - if (newJob == nil && oldJob != nil) || (newJob != nil && oldJob == nil) { - logger.V(2).Info("cronjob work diff") - return true - } - if newJob.Name != oldJob.Name || - !reflect.DeepEqual(newJob.Labels, oldJob.Labels) { - logger.V(2).Info("cronjob labels diff") - return true - } - - // Spec - if !reflect.DeepEqual(newJob.Spec.FailedJobsHistoryLimit, oldJob.Spec.FailedJobsHistoryLimit) || - !reflect.DeepEqual(newJob.Spec.SuccessfulJobsHistoryLimit, oldJob.Spec.SuccessfulJobsHistoryLimit) || - newJob.Spec.Schedule != oldJob.Spec.Schedule { - logger.V(2).Info("cronjob schedule diff", - "FailedJobsHistoryLimit", !reflect.DeepEqual(newJob.Spec.FailedJobsHistoryLimit, oldJob.Spec.FailedJobsHistoryLimit), - "SuccessfulJobsHistoryLimit", !reflect.DeepEqual(newJob.Spec.SuccessfulJobsHistoryLimit, oldJob.Spec.SuccessfulJobsHistoryLimit), - "Schedule", newJob.Spec.Schedule != oldJob.Spec.Schedule, - ) - return true - } - - // template - oldTpl := oldJob.Spec.JobTemplate - newTpl := newJob.Spec.JobTemplate - if !reflect.DeepEqual(newTpl.Labels, oldTpl.Labels) { - logger.V(2).Info("cronjob template labels diff") - return true - } - return IsPodTemplasteChanged(&newTpl.Spec.Template, &oldTpl.Spec.Template, logger) -} diff --git a/pkg/kubernetes/builder/clusterbuilder/redisbackup.go b/pkg/kubernetes/builder/clusterbuilder/redisbackup.go deleted file mode 100644 index b1a7c2a..0000000 --- a/pkg/kubernetes/builder/clusterbuilder/redisbackup.go +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package clusterbuilder - -import ( - "fmt" - - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/util" - - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" -) - -func GenerateCronJobName(redisName, scheduleName string) string { - return fmt.Sprintf("%s-%s", redisName, scheduleName) -} - -func NewRedisClusterBackupCronJobFromCR(schedule v1alpha1.Schedule, cluster *v1alpha1.DistributedRedisCluster) *batchv1.CronJob { - image := config.GetDefaultBackupImage() - if cluster.Spec.Backup.Image != "" { - image = cluster.Spec.Backup.Image - } - _labels := GetClusterLabels(cluster.Name, map[string]string{"redisclusterbackups.redis.middleware.alauda.io/instanceName": cluster.Name}) - delete(_labels, "redis.kun/name") - - job := &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: GenerateCronJobName(cluster.Name, schedule.Name), - Namespace: cluster.Namespace, - Labels: _labels, - OwnerReferences: util.BuildOwnerReferences(cluster), - }, - Spec: batchv1.CronJobSpec{ - Schedule: schedule.Schedule, - SuccessfulJobsHistoryLimit: &schedule.Keep, - FailedJobsHistoryLimit: &schedule.Keep, - JobTemplate: batchv1.JobTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: _labels, - }, - Spec: batchv1.JobSpec{ - BackoffLimit: pointer.Int32(0), - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: _labels, - }, - Spec: corev1.PodSpec{ - ServiceAccountName: RedisInstanceServiceAccountName, - RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{ - { - Name: "backup-schedule", - Image: image, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("200Mi"), - corev1.ResourceCPU: resource.MustParse("200m"), - }, - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("500Mi"), - corev1.ResourceCPU: resource.MustParse("500m"), - }, - }, - ImagePullPolicy: "Always", - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup schedule"}, - Env: []corev1.EnvVar{ - { - Name: "BACKUP_JOB_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "BACKUP_JOB_UID", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.uid", - }, - }, - }, - { - Name: "BACKUP_IMAGE", - Value: image, - }, - { - Name: "REDIS_CLUSTER_NAME", - Value: cluster.Name, - }, - { - Name: "STORAGE_CLASS_NAME", - Value: schedule.Storage.StorageClassName, - }, - { - Name: "STORAGE_SIZE", - Value: schedule.Storage.Size.String(), - }, - { - Name: "SCHEDULE_NAME", - Value: schedule.Name, - }, - }, - SecurityContext: &corev1.SecurityContext{}, - }, - }, - SecurityContext: &corev1.PodSecurityContext{}, - }, - }, - }, - }, - }, - } - if schedule.KeepAfterDeletion { - job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env = append(job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: "KEEP_AFTER_DELETION", - Value: "true", - }) - } - if schedule.Target.S3Option.S3Secret != "" { - job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env = append(job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: "BACKOFF_LIMIT", - Value: "6", - }, - corev1.EnvVar{ - Name: "S3_BUCKET_NAME", - Value: schedule.Target.S3Option.Bucket, - }, - corev1.EnvVar{ - Name: "S3_SECRET", - Value: schedule.Target.S3Option.S3Secret, - }, - ) - } - return job -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/acl.go b/pkg/kubernetes/builder/sentinelbuilder/acl.go deleted file mode 100644 index 86bacdd..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/acl.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - "fmt" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/redis/v1" - security "github.com/alauda/redis-operator/pkg/security/password" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func GenerateSentinelACLConfigMapName(name string) string { - return fmt.Sprintf("rfr-acl-%s", name) -} - -// acl operator secret -func GenerateSentinelACLOperatorSecretName(name string) string { - return fmt.Sprintf("rfr-acl-%s-operator-secret", name) -} - -func NewSentinelOpSecret(rf *databasesv1.RedisFailover) *corev1.Secret { - randPassword, _ := security.GeneratePassword(security.MaxPasswordLen) - - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: GenerateSentinelACLOperatorSecretName(rf.Name), - Namespace: rf.Namespace, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "password": []byte(randPassword), - "username": []byte("operator"), - }, - } -} - -func NewSentinelAclConfigMap(rf *databasesv1.RedisFailover, data map[string]string) *corev1.ConfigMap { - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: GenerateSentinelACLConfigMapName(rf.Name), - Namespace: rf.Namespace, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Data: data, - } -} - -func GenerateSentinelOperatorsRedisUserName(name string) string { - return fmt.Sprintf("rfr-acl-%s-operator", name) -} - -func GenerateSentinelOperatorsRedisUser(st types.RedisFailoverInstance, passwordsecret string) redismiddlewarealaudaiov1.RedisUser { - passwordsecrets := []string{} - if passwordsecret != "" { - passwordsecrets = append(passwordsecrets, passwordsecret) - } - rule := "~* +@all" - if st.Version().IsACL2Supported() { - rule = "~* &* +@all" - } - return redismiddlewarealaudaiov1.RedisUser{ - ObjectMeta: metav1.ObjectMeta{ - Name: GenerateSentinelOperatorsRedisUserName(st.GetName()), - Namespace: st.GetNamespace(), - }, - Spec: redismiddlewarealaudaiov1.RedisUserSpec{ - AccountType: redismiddlewarealaudaiov1.System, - Arch: redis.SentinelArch, - RedisName: st.GetName(), - Username: "operator", - PasswordSecrets: passwordsecrets, - AclRules: rule, - }, - } -} - -func GenerateSentinelDefaultRedisUserName(name string) string { - return fmt.Sprintf("rfr-acl-%s-default", name) -} - -func GenerateSentinelDefaultRedisUser(rf *databasesv1.RedisFailover, passwordsecret string) redismiddlewarealaudaiov1.RedisUser { - passwordsecrets := []string{} - if passwordsecret != "" { - passwordsecrets = append(passwordsecrets, passwordsecret) - } - return redismiddlewarealaudaiov1.RedisUser{ - ObjectMeta: metav1.ObjectMeta{ - Name: GenerateSentinelDefaultRedisUserName(rf.Name), - Namespace: rf.Namespace, - }, - Spec: redismiddlewarealaudaiov1.RedisUserSpec{ - AccountType: redismiddlewarealaudaiov1.Default, - Arch: redis.SentinelArch, - RedisName: rf.Name, - Username: "default", - PasswordSecrets: passwordsecrets, - AclRules: "allkeys +@all -acl -flushall -flushdb -keys", - }, - } -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/configmap.go b/pkg/kubernetes/builder/sentinelbuilder/configmap.go deleted file mode 100644 index 33b8462..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/configmap.go +++ /dev/null @@ -1,453 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - "bytes" - "fmt" - "path" - "sort" - "strings" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/util" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - RedisConfig_MaxMemory = "maxmemory" - RedisConfig_MaxMemoryPolicy = "maxmemory-policy" - RedisConfig_ClientOutputBufferLimit = "client-output-buffer-limit" - RedisConfig_Save = "save" - RedisConfig_RenameCommand = "rename-command" - RedisConfig_Appendonly = "appendonly" - RedisConfig_ReplDisklessSync = "repl-diskless-sync" -) - -var MustQuoteRedisConfig = map[string]struct{}{ - "tls-protocols": {}, -} - -var MustUpperRedisConfig = map[string]struct{}{ - "tls-ciphers": {}, - "tls-ciphersuites": {}, - "tls-protocols": {}, -} - -func NewSentinelConfigMap(rf *databasesv1.RedisFailover, selectors map[string]string) *corev1.ConfigMap { - name := GetSentinelDeploymentName(rf.Name) - namespace := rf.Namespace - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleSEN, rf.Name), selectors) - sentinelConfigContent := `sentinel monitor mymaster 127.0.0.1 6379 2 -sentinel down-after-milliseconds mymaster 1000 -sentinel failover-timeout mymaster 3000 -sentinel parallel-syncs mymaster 2` - - version, _ := redis.ParseRedisVersionFromImage(rf.Spec.Sentinel.Image) - innerConfigs := version.CustomConfigs(redis.SentinelArch) - for k, v := range innerConfigs { - sentinelConfigContent += fmt.Sprintf("\n%s %s", k, v) - } - - sentinelEntrypointTemplate := `#!/bin/sh - -redis-server %s --sentinel $* | sed 's/auth-pass .*/auth-pass ******/'` - - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Data: map[string]string{ - util.SentinelConfigFileName: sentinelConfigContent, - util.SentinelEntrypoint: fmt.Sprintf( - sentinelEntrypointTemplate, - path.Join("/redis", util.SentinelConfigFileName), - ), - }, - } -} - -func NewSentinelProbeConfigMap(rf *databasesv1.RedisFailover, selectors map[string]string) *corev1.ConfigMap { - name := GetSentinelReadinessConfigMapName(rf.Name) - namespace := rf.Namespace - tlsOptions := "" - if rf.Spec.EnableTLS { - tlsOptions = GenerateRedisTLSOptions() - } - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleSEN, rf.Name), selectors) - checkContent := `#!/usr/bin/env sh -set -eou pipefail -redis-cli -p 26379 %s -h $(hostname) ping -slaves=$(redis-cli -p 26379 %s info sentinel|grep master0| grep -Eo 'slaves=[0-9]+' | awk -F= '{print $2}') -status=$(redis-cli -p 26379 %s info sentinel|grep master0| grep -Eo 'status=\w+' | awk -F= '{print $2}') -if [ "$status" != "ok" ]; then - exit 1 -fi -if [ $slaves -lt 0 ]; then - exit 1 -fi` - - checkContent = fmt.Sprintf(checkContent, tlsOptions, tlsOptions, tlsOptions) - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Data: map[string]string{ - "readiness.sh": checkContent, - }, - } -} - -func NewRedisScriptConfigMap(rf *databasesv1.RedisFailover, selectors map[string]string) *corev1.ConfigMap { - name := GetRedisScriptConfigMapName(rf.Name) - namespace := rf.Namespace - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleRedis, rf.Name), selectors) - envSentinelHost := GetEnvSentinelHost(rf.Name) - envSentinelPort := GetEnvSentinelPort(rf.Name) - tlsOptions := "" - if rf.Spec.EnableTLS { - tlsOptions = GenerateRedisTLSOptions() - } - ipOptions := "" - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - ipOptions = "-h ::1" - } - shutdownContent := fmt.Sprintf(`#!/bin/sh - ANNOUNCE_CONFIG="/data/announce.conf" - master="" - response_code="" - echo "Asking sentinel who is master..." - master=$(redis-cli -h ${%s} -p ${%s} %s --csv SENTINEL get-master-addr-by-name mymaster | tr ',' ' ' | tr -d '\"' |cut -d' ' -f1) - echo "Master is $master,Saving rdb" - REDIS_PASSWORD=$(cat /account/password) - REDIS_USERNAME=$(cat /account/username) - if [[ $REDIS_PASSWORD ]]; then - if [[ $REDIS_USERNAME ]]; then - redis-cli -a "$REDIS_PASSWORD" --user $REDIS_USERNAME %s %s SAVE - else - redis-cli -a "$REDIS_PASSWORD" %s %s SAVE - fi - else - redis-cli %s %s SAVE - fi - announce_ip="NULL" - if [ -f ${ANNOUNCE_CONFIG} ]; then - announce_ip=$(cat ${ANNOUNCE_CONFIG} |grep announce-ip|awk '{print $2}') - fi - if [ $master = $(hostname -i) ] || [ $master = $announce_ip ]; then - while [ ! "$response_code" = "OK" ]; do - response_code=$(redis-cli -h ${%s} -p ${%s} %s SENTINEL failover mymaster) - echo "after failover with code $response_code" - sleep 1 - done - fi`, envSentinelHost, envSentinelPort, tlsOptions, ipOptions, tlsOptions, ipOptions, tlsOptions, ipOptions, tlsOptions, envSentinelHost, envSentinelPort, tlsOptions) - startContent := fmt.Sprintf(`#!/bin/sh - REDIS_PASSWORD=$(cat /account/password) - REDIS_USERNAME=$(cat /account/username) - if [[ $REDIS_PASSWORD ]]; then - if [[ $REDIS_USERNAME ]]; then - output=$(redis-cli -a "$REDIS_PASSWORD" --user $REDIS_USERNAME %s %s INFO|grep '^loading\|master_sync_in_progress\|^role:' || echo "") - else - output=$(redis-cli -a "$REDIS_PASSWORD" %s %s INFO|grep '^loading\|master_sync_in_progress\|^role:' || echo "") - fi - else - output=$(redis-cli %s %s INFO |grep '^loading\|master_sync_in_progress\|^role:' || echo "") - fi - - if [[ "$output" == *"loading:0"* ]]; then - if [[ ! "$output" == *"role:master"* ]]; then - echo "Redis is not master,check master_sync_in_progress" - if [[ "$output" == *"master_sync_in_progress:0"* ]]; then - echo "Redis loading is 0 and master_sync_in_progress is 0. Script exiting normally." - exit 0 - else - echo "master_sync_in_progress is not 0. Script exiting with an error." - exit 1 - fi - fi - - echo "master exit 0" - exit 0 - else - echo "Redis loading is not 0 . Script exiting with an error." - exit 1 - fi`, ipOptions, tlsOptions, ipOptions, tlsOptions, ipOptions, tlsOptions) - - autoSlaveContent := fmt.Sprintf(`#!/bin/sh -echo "check sentinel status" -status=$(redis-cli -h ${%s} -p ${%s} %s info sentinel|grep master0| grep -Eo 'status=\w+' | awk -F= '{print $2}' || echo "") -if [ "$status" != "ok" ]; then - exit 0 -fi - -echo "get master" -master=$(redis-cli -h ${%s} -p ${%s} %s --csv SENTINEL get-master-addr-by-name mymaster | tr ',' ' ' | tr -d '\"' |cut -d' ' -f1 || echo "") -masterPort=$(redis-cli -h ${%s} -p ${%s} %s --csv SENTINEL get-master-addr-by-name mymaster | tr ',' ' ' | tr -d '\"' |cut -d' ' -f2 || echo "") - -echo "master: $master" -echo "masterPort: $masterPort" - -if [ "$master" = "" ] || [ "$masterPort" = "" ]; then - exit 0 -fi - -if [ "$master" != "127.0.0.1" ] || [ "$master" = "$(hostname -i)" ] || [ "$master" = "$announce_ip" ]; then - echo "slaveof $master $masterPort" > /data/slaveof.conf -fi`, envSentinelHost, envSentinelPort, tlsOptions, envSentinelHost, envSentinelPort, tlsOptions, envSentinelHost, envSentinelPort, tlsOptions) - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Data: map[string]string{ - "shutdown.sh": shutdownContent, - "start.sh": startContent, - "auto_replica.sh": autoSlaveContent, - }, - } -} - -func GetEnvSentinelHost(name string) string { - return "RFS_REDIS_SERVICE_HOST" -} - -func GetEnvSentinelPort(name string) string { - return "RFS_REDIS_SERVICE_PORT_SENTINEL" -} - -func NewRedisConfigMap(st types.RedisFailoverInstance, selectors map[string]string) *corev1.ConfigMap { - rf := st.Definition() - customConfig := rf.Spec.Redis.CustomConfig - - default_config := make(map[string]string) - default_config["loglevel"] = "notice" - default_config["stop-writes-on-bgsave-error"] = "yes" - default_config["rdbcompression"] = "yes" - default_config["rdbchecksum"] = "yes" - default_config["slave-read-only"] = "yes" - default_config["repl-diskless-sync"] = "no" - default_config["slowlog-max-len"] = "128" - default_config["slowlog-log-slower-than"] = "10000" - default_config["maxclients"] = "10000" - default_config["hz"] = "50" - default_config["timeout"] = "60" - default_config["tcp-keepalive"] = "300" - default_config["tcp-backlog"] = "511" - default_config["protected-mode"] = "no" - - version, _ := redis.ParseRedisVersionFromImage(rf.Spec.Redis.Image) - innerRedisConfig := version.CustomConfigs(redis.SentinelArch) - default_config = util.MergeMap(default_config, innerRedisConfig) - - for k, v := range customConfig { - k = strings.ToLower(k) - v = strings.TrimSpace(v) - if k == "save" && v == "60 100" { - continue - } - default_config[k] = v - } - - // check if it's need to set default save - // check if aof enabled - if customConfig[RedisConfig_Appendonly] != "yes" && - customConfig[RedisConfig_ReplDisklessSync] != "yes" && - (customConfig[RedisConfig_Save] == "" || customConfig[RedisConfig_Save] == `""`) { - - default_config["save"] = "60 10000 300 100 600 1" - } - - if limits := rf.Spec.Redis.Resources.Limits; limits != nil { - if configedMem := customConfig[RedisConfig_MaxMemory]; configedMem == "" { - memLimit, _ := limits.Memory().AsInt64() - if policy := customConfig[RedisConfig_MaxMemoryPolicy]; policy == "noeviction" { - memLimit = int64(float64(memLimit) * 0.8) - } else { - memLimit = int64(float64(memLimit) * 0.7) - } - if memLimit > 0 { - default_config[RedisConfig_MaxMemory] = fmt.Sprintf("%d", memLimit) - } - } - } - - renameConfig := map[string]string{} - if renameVal := customConfig[RedisConfig_RenameCommand]; renameVal != "" { - fields := strings.Fields(strings.ToLower(renameVal)) - if len(fields)%2 == 0 { - for i := 0; i < len(fields); i += 2 { - renameConfig[fields[i]] = fields[i+1] - } - } - } - var renameVal []string - for k, v := range renameConfig { - if k == v || k == "config" { - continue - } - if v == "" { - v = `""` - } - renameVal = append(renameVal, k, v) - } - if len(renameVal) > 0 { - default_config[RedisConfig_RenameCommand] = strings.Join(renameVal, " ") - } - - if st.Version().IsACLSupported() { - delete(default_config, RedisConfig_RenameCommand) - } - - keys := make([]string, 0, len(default_config)) - for k := range default_config { - keys = append(keys, k) - } - sort.Strings(keys) - - var buffer bytes.Buffer - for _, k := range keys { - v := default_config[k] - if v == "" || v == `""` { - buffer.WriteString(fmt.Sprintf("%s \"\"\n", k)) - continue - } - switch k { - case RedisConfig_ClientOutputBufferLimit: - fields := strings.Fields(v) - if len(fields)%4 != 0 { - continue - } - for i := 0; i < len(fields); i += 4 { - buffer.WriteString(fmt.Sprintf("%s %s %s %s %s\n", k, fields[i], fields[i+1], fields[i+2], fields[i+3])) - } - case RedisConfig_Save, RedisConfig_RenameCommand: - fields := strings.Fields(v) - if len(fields)%2 != 0 { - continue - } - for i := 0; i < len(fields); i += 2 { - buffer.WriteString(fmt.Sprintf("%s %s %s\n", k, fields[i], fields[i+1])) - } - default: - if _, ok := MustQuoteRedisConfig[k]; ok && !strings.HasPrefix(v, `"`) { - v = fmt.Sprintf(`"%s"`, v) - } - if _, ok := MustUpperRedisConfig[k]; ok { - v = strings.ToUpper(v) - } - buffer.WriteString(fmt.Sprintf("%s %s\n", k, v)) - } - } - - entrypoint := `#!/bin/sh -CONFIG_FILE="/tmp/redis.conf" -cat /redis/redis.conf > $CONFIG_FILE -REDIS_PASSWORD=$(cat /account/password) -REDIS_USERNAME=$(cat /account/username) -ACL_ARGS="" -ACL_CONFIG="/tmp/acl.conf" - -if [ ! -z "${REDIS_PASSWORD}" ]; then - echo "requirepass \"${REDIS_PASSWORD}\"" >> $CONFIG_FILE - echo "masterauth \"${REDIS_PASSWORD}\"" >> $CONFIG_FILE -fi - -if [ -e /tmp/newpass ]; then - echo "new passwd found" - REDIS_PASSWORD=$(cat /tmp/newpass) -fi - -if [ ! -z "${REDIS_USERNAME}" ]; then - echo "masteruser \"${REDIS_USERNAME}\"" >> $CONFIG_FILE -fi - -if [[ ! -z "${ACL_CONFIGMAP_NAME}" ]]; then - echo "# Run: generate acl " - echo "${ACL_CONFIGMAP_NAME} ${POD_NAMESPACE}" - /opt/redis-tools helper generate acl --name ${ACL_CONFIGMAP_NAME} --namespace ${POD_NAMESPACE} >> ${ACL_CONFIG} || exit 1 - ACL_ARGS="--aclfile ${ACL_CONFIG}" -fi - -ANNOUNCE_CONFIG="/data/announce.conf" -if [ -f ${ANNOUNCE_CONFIG} ]; then - echo "append announce conf to redis config" - echo "" >> $CONFIG_FILE - cat ${ANNOUNCE_CONFIG} >> $CONFIG_FILE -fi - - -SLAVEOF_CONFIG="/data/slaveof.conf" -if [ -f ${SLAVEOF_CONFIG} ]; then - echo "append slaveof conf to redis config" - echo "" >> $CONFIG_FILE - cat ${SLAVEOF_CONFIG} >> $CONFIG_FILE -else - echo "slaveof 127.0.0.1 6379" >> $CONFIG_FILE -fi - -POD_IPS_LIST=$(echo "${POD_IPS}"|sed 's/,/ /g') -ARGS="" -if [ ! -z "${POD_IPS}" ]; then - if [[ ${IP_FAMILY_PREFER} == "IPv6" ]]; then - ARGS="${ARGS} --bind ${POD_IPS_LIST} ::1" - else - ARGS="${ARGS} --bind ${POD_IPS_LIST} 127.0.0.1" - fi -fi -chmod 0640 $CONFIG_FILE -chmod 0640 $ACL_CONFIG - -redis-server $CONFIG_FILE ${ACL_ARGS} ${ARGS} $@` - - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: GetRedisConfigMapName(rf), - Namespace: rf.Namespace, - Labels: GetCommonLabels(rf.Name, selectors), - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Data: map[string]string{ - util.RedisConfigFileName: buffer.String(), - util.SentinelEntrypoint: entrypoint, - }, - } -} - -func GetRedisConfigMapName(rf *databasesv1.RedisFailover) string { - version := config.GetRedisVersion(rf.Spec.Redis.Image) - name := GetSentinelStatefulSetName(rf.Name) - if version != "" { - name = name + "-" + version - } - return name -} - -func GetRedisScriptConfigMapName(name string) string { - return fmt.Sprintf("rfr-s-%s", name) -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/cronjob.go b/pkg/kubernetes/builder/sentinelbuilder/cronjob.go deleted file mode 100644 index 3c42dbc..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/cronjob.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - "fmt" - - batchv1 "k8s.io/api/batch/v1" - "k8s.io/utils/pointer" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/util" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func GenerateCronJobName(redisName, scheduleName string) string { - return fmt.Sprintf("%s-%s", redisName, scheduleName) -} - -func NewRedisFailoverBackupCronJobFromCR(schedule v1alpha1.Schedule, rf *databasesv1.RedisFailover, selectors map[string]string) *batchv1.CronJob { - image := config.GetDefaultBackupImage() - - if rf.Spec.Redis.Backup.Image != "" { - image = rf.Spec.Redis.Backup.Image - } - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(util.RedisBackupRoleName, rf.Name)) - job := &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: GenerateCronJobName(rf.Name, schedule.Name), - Namespace: rf.Namespace, - Labels: labels, - OwnerReferences: util.BuildOwnerReferences(rf), - }, - Spec: batchv1.CronJobSpec{ - Schedule: schedule.Schedule, - SuccessfulJobsHistoryLimit: &schedule.Keep, - FailedJobsHistoryLimit: &schedule.Keep, - JobTemplate: batchv1.JobTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: batchv1.JobSpec{ - BackoffLimit: pointer.Int32(0), - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - ServiceAccountName: clusterbuilder.RedisInstanceServiceAccountName, - RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{ - { - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("200Mi"), - corev1.ResourceCPU: resource.MustParse("200m"), - }, - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("500Mi"), - corev1.ResourceCPU: resource.MustParse("500m"), - }, - }, - Name: "backup-schedule", - Image: image, - ImagePullPolicy: "Always", - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup schedule"}, - Env: []corev1.EnvVar{ - { - Name: "BACKUP_JOB_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "BACKUP_JOB_UID", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.uid", - }, - }, - }, - { - Name: "BACKUP_IMAGE", - Value: image, - }, - { - Name: "REDIS_FAILOVER_NAME", - Value: rf.Name, - }, - { - Name: "STORAGE_CLASS_NAME", - Value: schedule.Storage.StorageClassName, - }, - { - Name: "STORAGE_SIZE", - Value: schedule.Storage.Size.String(), - }, - { - Name: "SCHEDULE_NAME", - Value: schedule.Name, - }, - }, - SecurityContext: &corev1.SecurityContext{}, - }, - }, - SecurityContext: &corev1.PodSecurityContext{}, - }, - }, - }, - }, - }, - } - if schedule.KeepAfterDeletion { - job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env = append(job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: "KEEP_AFTER_DELETION", - Value: "true", - }) - } - if schedule.Target.S3Option.S3Secret != "" { - job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env = append(job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: "REDIS_NAME", - Value: rf.Name, - }, - corev1.EnvVar{ - Name: "BACKOFF_LIMIT", - Value: "6", - }, - corev1.EnvVar{ - Name: "S3_BUCKET_NAME", - Value: schedule.Target.S3Option.Bucket, - }, - corev1.EnvVar{ - Name: "S3_SECRET", - Value: schedule.Target.S3Option.S3Secret, - }, - ) - } - return job -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/deployment.go b/pkg/kubernetes/builder/sentinelbuilder/deployment.go deleted file mode 100644 index a9e76da..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/deployment.go +++ /dev/null @@ -1,289 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - "fmt" - "path" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - appv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/utils/pointer" -) - -func GenerateSentinelDeployment(rf *databasesv1.RedisFailover, selectors map[string]string) *appv1.Deployment { - name := util.GetSentinelName(rf) - namespace := rf.Namespace - if len(selectors) == 0 { - selectors = MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(util.SentinelRoleName, rf.Name)) - } else { - selectors = MergeMap(selectors, GenerateSelectorLabels(util.SentinelRoleName, rf.Name)) - } - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(util.SentinelRoleName, rf.Name), selectors) - - const configMountPathPrefix = "/conf" - sentinelCommand := getSentinelCommand(configMountPathPrefix, rf) - localhost := "127.0.0.1" - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - localhost = "::1" - } - deploy := &appv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - Annotations: util.GenerateRedisRebuildAnnotation(), - }, - Spec: appv1.DeploymentSpec{ - Replicas: &rf.Spec.Sentinel.Replicas, - Strategy: appv1.DeploymentStrategy{ - Type: "RollingUpdate", - RollingUpdate: &appv1.RollingUpdateDeployment{ - MaxUnavailable: &intstr.IntOrString{Type: intstr.String, StrVal: "35%"}, - }, - }, - Selector: &metav1.LabelSelector{ - MatchLabels: selectors, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: selectors, - Annotations: rf.Spec.Sentinel.PodAnnotations, - }, - Spec: corev1.PodSpec{ - HostAliases: []corev1.HostAlias{ - { - IP: localhost, - Hostnames: []string{LocalInjectName}, - }, - }, - Affinity: getAffinity(rf.Spec.Sentinel.Affinity, selectors), - Tolerations: rf.Spec.Sentinel.Tolerations, - NodeSelector: rf.Spec.Sentinel.NodeSelector, - SecurityContext: getSecurityContext(rf.Spec.Sentinel.SecurityContext), - ImagePullSecrets: rf.Spec.Sentinel.ImagePullSecrets, - InitContainers: []corev1.Container{ - { - Name: "sentinel-config-copy", - Image: rf.Spec.Sentinel.Image, - ImagePullPolicy: pullPolicy(rf.Spec.Sentinel.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: "sentinel-config", - MountPath: "/redis", - }, - { - Name: "sentinel-config-writable", - MountPath: "/redis-writable", - }, - }, - Command: []string{ - "cp", - "-n", - fmt.Sprintf("/redis/%s", util.SentinelConfigFileName), - fmt.Sprintf("/redis-writable/%s", util.SentinelConfigFileName), - }, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("10m"), - corev1.ResourceMemory: resource.MustParse("32Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("10m"), - corev1.ResourceMemory: resource.MustParse("32Mi"), - }, - }, - }, - }, - Containers: []corev1.Container{ - { - Name: "sentinel", - Image: rf.Spec.Sentinel.Image, - ImagePullPolicy: pullPolicy(rf.Spec.Sentinel.ImagePullPolicy), - Ports: []corev1.ContainerPort{ - { - Name: "sentinel", - ContainerPort: 26379, - Protocol: corev1.ProtocolTCP, - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "readiness-probe", - MountPath: "/redis-probe", - }, - { - Name: "sentinel-config-writable", - MountPath: "/redis", - }, - { - Name: "sentinel-config", - MountPath: configMountPathPrefix, - }, - }, - Command: sentinelCommand, - Lifecycle: &corev1.Lifecycle{ - PreStop: &corev1.LifecycleHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "sh", - "-c", - "redis-cli -h local.inject -p 26379 sentinel flushconfig", - }, - }, - }, - }, - ReadinessProbe: &corev1.Probe{ - PeriodSeconds: 15, - FailureThreshold: 5, - TimeoutSeconds: 5, - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "sh", - "/redis-probe/readiness.sh", - }, - }, - }, - }, - LivenessProbe: &corev1.Probe{ - InitialDelaySeconds: 30, - PeriodSeconds: 60, - FailureThreshold: 5, - TimeoutSeconds: 5, - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"redis-cli", "-h", "local.inject", "-p", "26379", "ping"}, - }, - }, - }, - Resources: rf.Spec.Sentinel.Resources, - SecurityContext: &corev1.SecurityContext{ - ReadOnlyRootFilesystem: pointer.Bool(true), - }, - }, - }, - Volumes: []corev1.Volume{ - { - Name: "sentinel-config", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: util.GetSentinelName(rf), - }, - }, - }, - }, - { - Name: "readiness-probe", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: util.GetSentinelReadinessConfigmap(rf), - }, - }, - }, - }, - { - Name: "sentinel-config-writable", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - }, - }, - }, - } - if rf.Spec.EnableTLS { - deploy.Spec.Template.Spec.Volumes = append(deploy.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: RedisTLSVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: util.GetRedisSSLSecretName(rf.Name), - }, - }, - }) - deploy.Spec.Template.Spec.Containers[0].VolumeMounts = append(deploy.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: RedisTLSVolumeName, - MountPath: "/tls", - }) - } - return deploy -} - -func getSentinelCommand(pathPrefix string, rf *databasesv1.RedisFailover) []string { - if len(rf.Spec.Sentinel.Command) > 0 { - return rf.Spec.Sentinel.Command - } - - command := []string{"sh", path.Join(pathPrefix, util.SentinelEntrypoint)} - if rf.Spec.EnableTLS { - command = append(command, getSentinelTLSCommand()...) - } - return command - -} - -func getSentinelTLSCommand() []string { - return []string{ - "--tls-port", - "26379", - "--port", - "0", - "--tls-cert-file", - "/tls/tls.crt", - "--tls-key-file", - "/tls/tls.key", - "--tls-ca-cert-file", - "/tls/ca.crt", - "--tls-replication", - "yes", - } -} - -func DiffDeployment(new *appv1.Deployment, old *appv1.Deployment, logger logr.Logger) bool { - if new.Spec.Replicas != nil && old.Spec.Replicas != nil && *new.Spec.Replicas != *old.Spec.Replicas { - return true - } - if new.Spec.Strategy.Type != old.Spec.Strategy.Type { - return true - } - if new.Spec.Strategy.RollingUpdate != nil && old.Spec.Strategy.RollingUpdate != nil { - if new.Spec.Strategy.RollingUpdate.MaxUnavailable != nil && old.Spec.Strategy.RollingUpdate.MaxUnavailable != nil { - if new.Spec.Strategy.RollingUpdate.MaxUnavailable.Type != old.Spec.Strategy.RollingUpdate.MaxUnavailable.Type { - return true - } - if new.Spec.Strategy.RollingUpdate.MaxUnavailable.StrVal != old.Spec.Strategy.RollingUpdate.MaxUnavailable.StrVal { - return true - } - if new.Spec.Strategy.RollingUpdate.MaxUnavailable.IntVal != old.Spec.Strategy.RollingUpdate.MaxUnavailable.IntVal { - return true - } - } - } - return clusterbuilder.IsPodTemplasteChanged(&new.Spec.Template, &old.Spec.Template, logger) -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/helper.go b/pkg/kubernetes/builder/sentinelbuilder/helper.go deleted file mode 100644 index f2e2b1b..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/helper.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - "fmt" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - LabelRedisArch = "redisarch" - RestoreContainerName = "restore" -) - -func MergeMap(extraMap ...map[string]string) map[string]string { - result := make(map[string]string) - for _, item := range extraMap { - for k, v := range item { - result[k] = v - } - } - return result -} - -func GetCommonLabels(name string, extra ...map[string]string) map[string]string { - labels := getPublicLabels(name) - for _, item := range extra { - for k, v := range item { - labels[k] = v - } - } - return labels -} - -func getPublicLabels(name string) map[string]string { - return map[string]string{ - "redisfailovers.databases.spotahome.com/name": name, - "app.kubernetes.io/managed-by": "redis-operator", - "middleware.instance/name": name, - "middleware.instance/type": "redis-failover", - } -} - -func GenerateSelectorLabels(component, name string) map[string]string { - return map[string]string{ - "app.kubernetes.io/part-of": "redis-failover", - "app.kubernetes.io/component": component, - "app.kubernetes.io/name": name, - } -} - -func GetSentinelStatefulSetName(sentinelName string) string { - return fmt.Sprintf("rfr-%s", sentinelName) -} - -func GetSentinelDeploymentName(sentinelName string) string { - return fmt.Sprintf("rfs-%s", sentinelName) -} - -func GetSentinelServiceName(sentinelName string) string { - return fmt.Sprintf("rfs-%s", sentinelName) -} - -func GetSentinelReadinessConfigMapName(sentinelName string) string { - return fmt.Sprintf("rfs-r-%s", sentinelName) -} - -func GenerateRedisTLSOptions() string { - return "--tls --cert /tls/tls.crt --key /tls/tls.key --cacert /tls/ca.crt" -} - -func GetOwnerReferenceForRedisFailover(rf *databasesv1.RedisFailover) []metav1.OwnerReference { - rcvk := databasesv1.GroupVersion.WithKind("RedisFailover") - owref := []metav1.OwnerReference{ - *metav1.NewControllerRef(rf, rcvk), - } - *owref[0].BlockOwnerDeletion = false - return owref -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/helper_test.go b/pkg/kubernetes/builder/sentinelbuilder/helper_test.go deleted file mode 100644 index 27b9a7c..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/helper_test.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - "reflect" - "testing" -) - -func TestMergeMap(t *testing.T) { - t.Run("MergeMap with empty maps", func(t *testing.T) { - // Test merging empty maps - merged := MergeMap() - expected := map[string]string{} - if !reflect.DeepEqual(merged, expected) { - t.Errorf("MergeMap() = %v, want %v", merged, expected) - } - }) - - t.Run("MergeMap with non-empty maps", func(t *testing.T) { - // Test merging non-empty maps - map1 := map[string]string{"a": "1", "b": "2"} - map2 := map[string]string{"b": "3", "c": "4"} - map3 := map[string]string{"d": "5"} - map3 = nil - - merged := MergeMap(map1, map2, map3) - expected := map[string]string{"a": "1", "b": "3", "c": "4"} - - if !reflect.DeepEqual(merged, expected) { - t.Errorf("MergeMap() = %v, want %v", merged, expected) - } - }) -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/poddisruptionbudget.go b/pkg/kubernetes/builder/sentinelbuilder/poddisruptionbudget.go deleted file mode 100644 index 8934adf..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/poddisruptionbudget.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" -) - -func NewDeployPodDisruptionBudgetForCR(rf *databasesv1.RedisFailover, selectors map[string]string) *policyv1.PodDisruptionBudget { - maxUnavailable := intstr.FromInt(2) - namespace := rf.Namespace - selectors = MergeMap(selectors, GenerateSelectorLabels(RedisArchRoleSEN, rf.Name)) - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleSEN, rf.Name), selectors) - - name := GetSentinelDeploymentName(rf.Name) - return &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - Name: name, - Namespace: namespace, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - MaxUnavailable: &maxUnavailable, - Selector: &metav1.LabelSelector{ - MatchLabels: selectors, - }, - }, - } -} - -func NewPodDisruptionBudgetForCR(rf *databasesv1.RedisFailover, selectors map[string]string) *policyv1.PodDisruptionBudget { - maxUnavailable := intstr.FromInt(2) - namespace := rf.Namespace - selectors = MergeMap(selectors, GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleRedis, rf.Name), selectors) - - name := GetSentinelStatefulSetName(rf.Name) - return &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - Name: name, - Namespace: namespace, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - MaxUnavailable: &maxUnavailable, - Selector: &metav1.LabelSelector{ - MatchLabels: selectors, - }, - }, - } -} diff --git a/pkg/kubernetes/builder/sentinelbuilder/statefulset.go b/pkg/kubernetes/builder/sentinelbuilder/statefulset.go deleted file mode 100644 index e232fe3..0000000 --- a/pkg/kubernetes/builder/sentinelbuilder/statefulset.go +++ /dev/null @@ -1,853 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinelbuilder - -import ( - "fmt" - "os" - "path" - "strconv" - "strings" - - v1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - redisbackup "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" - appv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" -) - -const ( - redisShutdownConfigurationVolumeName = "redis-shutdown-config" - redisStorageVolumeName = "redis-data" - exporterContainerName = "redis-exporter" - graceTime = 30 - PasswordENV = "REDIS_PASSWORD" - redisConfigurationVolumeName = "redis-config" - RedisTmpVolumeName = "redis-tmp" - RedisTLSVolumeName = "redis-tls" - LocalInjectName = "local.inject" - redisAuthName = "redis-auth" - redisOptName = "redis-opt" - OperatorUsername = "OPERATOR_USERNAME" - OperatorSecretName = "OPERATOR_SECRET_NAME" - ServerContainerName = "redis" - SentinelContainerName = "sentinel" - SentinelContainerPortName = "sentinel" -) - -func GetRedisRWServiceName(sentinelName string) string { - return fmt.Sprintf("rfr-%s-read-write", sentinelName) -} - -func GetRedisROServiceName(sentinelName string) string { - return fmt.Sprintf("rfr-%s-read-only", sentinelName) -} - -func GenerateRedisStatefulSet(rf *v1.RedisFailover, rb *redisbackup.RedisBackup, selectors map[string]string, acl string) *appv1.StatefulSet { - name := GetSentinelStatefulSetName(rf.Name) - namespace := rf.Namespace - - if len(selectors) == 0 { - selectors = MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) - } else { - selectors = MergeMap(selectors, GenerateSelectorLabels(RedisArchRoleRedis, rf.Name)) - } - labels := MergeMap(GetCommonLabels(rf.Name), GenerateSelectorLabels(RedisArchRoleRedis, rf.Name), selectors) - - redisCommand := getRedisCommand(rf) - volumeMounts := getRedisVolumeMounts(rf, acl) - secretName := rf.Spec.Auth.SecretPath - if acl != "" { - secretName = acl - } - volumes := getRedisVolumes(rf, secretName) - probeArg := "redis-cli %s" - tlsOptions := "" - if rf.Spec.EnableTLS { - tlsOptions = util.GenerateRedisTLSOptions() - } - probeArg = fmt.Sprintf(probeArg, tlsOptions) - if acl != "" { - probeArg = fmt.Sprintf("%s -a $(cat /account/password) --user $(cat /account/username) -h %s ping", probeArg, LocalInjectName) - } else if rf.Spec.Auth.SecretPath != "" { - probeArg = fmt.Sprintf("%s -a $(cat /account/password) -h %s ping", probeArg, LocalInjectName) - } else { - probeArg = fmt.Sprintf("%s -h %s ping", probeArg, LocalInjectName) - } - localhost := "127.0.0.1" - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - localhost = "::1" - } - ss := &appv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - OwnerReferences: GetOwnerReferenceForRedisFailover(rf), - Annotations: util.GenerateRedisRebuildAnnotation(), - }, - Spec: appv1.StatefulSetSpec{ - ServiceName: name, - Replicas: &rf.Spec.Redis.Replicas, - UpdateStrategy: appv1.StatefulSetUpdateStrategy{ - Type: "RollingUpdate", - }, - Selector: &metav1.LabelSelector{ - MatchLabels: selectors, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: selectors, - Annotations: rf.Spec.Redis.PodAnnotations, - }, - Spec: corev1.PodSpec{ - HostAliases: []corev1.HostAlias{ - { - IP: localhost, - Hostnames: []string{LocalInjectName}, - }, - }, - Affinity: getAffinity(rf.Spec.Redis.Affinity, selectors), - Tolerations: rf.Spec.Redis.Tolerations, - NodeSelector: rf.Spec.Redis.NodeSelector, - SecurityContext: getSecurityContext(rf.Spec.Redis.SecurityContext), - ImagePullSecrets: rf.Spec.Redis.ImagePullSecrets, - Containers: []corev1.Container{ - { - Name: "redis", - Image: rf.Spec.Redis.Image, - ImagePullPolicy: pullPolicy(rf.Spec.Redis.ImagePullPolicy), - Env: []corev1.EnvVar{ - { - Name: "POD_IP", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "status.podIP", - }, - }, - }, - { - Name: "POD_IPS", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "status.podIPs", - }, - }, - }, - }, - Ports: []corev1.ContainerPort{ - { - Name: "redis", - ContainerPort: 6379, - Protocol: corev1.ProtocolTCP, - }, - }, - VolumeMounts: volumeMounts, - Command: redisCommand, - StartupProbe: &corev1.Probe{ - InitialDelaySeconds: 5, - FailureThreshold: 10, - TimeoutSeconds: 5, - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "sh", - "/redis-shutdown/start.sh", - }, - }, - }, - }, - ReadinessProbe: &corev1.Probe{ - InitialDelaySeconds: graceTime, - TimeoutSeconds: 5, - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "sh", - "-c", - probeArg, - }, - }, - }, - }, - LivenessProbe: &corev1.Probe{ - InitialDelaySeconds: graceTime, - TimeoutSeconds: 5, - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"sh", "-c", probeArg}, - }, - }, - }, - Resources: rf.Spec.Redis.Resources, - Lifecycle: &corev1.Lifecycle{ - PreStop: &corev1.LifecycleHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"/bin/sh", "/redis-shutdown/shutdown.sh"}, - }, - }, - }, - SecurityContext: &corev1.SecurityContext{ - ReadOnlyRootFilesystem: pointer.Bool(true), - }, - }, - }, - Volumes: volumes, - }, - }, - }, - } - if rf.Spec.Redis.Storage.PersistentVolumeClaim != nil { - pvc := rf.Spec.Redis.Storage.PersistentVolumeClaim.DeepCopy() - if len(pvc.Spec.AccessModes) == 0 { - pvc.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce} - } - if !rf.Spec.Redis.Storage.KeepAfterDeletion { - // Set an owner reference so the persistent volumes are deleted when the rc is - pvc.OwnerReferences = GetOwnerReferenceForRedisFailover(rf) - } - if pvc.Name == "" { - pvc.Name = getRedisDataVolumeName(rf) - } - ss.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{ - *pvc, - } - } - ss.Spec.Template.Spec.Containers[0].Env = append(ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: util.GetEnvSentinelHost(rf.Name), - Value: util.GetSentinelName(rf), - }) - ss.Spec.Template.Spec.Containers[0].Env = append(ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: util.GetEnvSentinelPort(rf.Name), - Value: "26379", - }) - - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - ss.Spec.Template.Spec.Containers[0].Env = append(ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: "IP_FAMILY_PREFER", - Value: string(rf.Spec.Redis.IPFamilyPrefer), - }) - } - if rf.Spec.Redis.Restore.BackupName != "" { - restore := createRestoreContainer(rf) - if rb.Spec.Target.S3Option.S3Secret != "" { - restore = createRestoreContainerForS3(rf, rb) - } - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, restore) - if rb.Spec.Target.S3Option.S3Secret == "" { - backupVolumes := corev1.Volume{ - Name: util.RedisBackupVolumeName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: util.GetClaimName(rb.Status.Destination), - }, - }, - } - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, backupVolumes) - } else { - s3secretVolumes := corev1.Volume{ - Name: util.S3SecretVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{SecretName: rb.Spec.Target.S3Option.S3Secret}, - }, - } - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, s3secretVolumes) - } - } - - if rf.Spec.Expose.EnableNodePort || rf.Spec.Redis.IPFamilyPrefer != "" { - expose := createExposeContainer(rf) - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, expose) - ss.Spec.Template.Spec.ServiceAccountName = clusterbuilder.RedisInstanceServiceAccountName - } - - if acl != "" { - ss.Spec.Template.Spec.ServiceAccountName = clusterbuilder.RedisInstanceServiceAccountName - ss.Spec.Template.Spec.Containers[0].Env = append(ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: OperatorSecretName, - Value: GenerateSentinelACLOperatorSecretName(rf.Name), - }) - ss.Spec.Template.Spec.Containers[0].Env = append(ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: OperatorUsername, - Value: user.DefaultOperatorUserName, - }) - ss.Spec.Template.Spec.Containers[0].Env = append(ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: "ACL_CONFIGMAP_NAME", - Value: GenerateSentinelACLConfigMapName(rf.Name), - }) - ss.Spec.Template.Spec.Containers[0].Env = append(ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ - Name: "POD_NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - }) - - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, createRedisToolInitContainer(rf)) - } - - if rf.Spec.Redis.Storage.PersistentVolumeClaim == nil { - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, createAutoReplicaContainer(rf, acl)) - } - - if rf.Spec.Redis.Exporter.Enabled { - defaultAnnotations := map[string]string{ - "prometheus.io/scrape": "true", - "prometheus.io/port": "http", - "prometheus.io/path": "/metrics", - } - ss.Spec.Template.Annotations = util.MergeMap(ss.Spec.Template.Annotations, defaultAnnotations) - - exporter := createRedisExporterContainer(rf, acl) - ss.Spec.Template.Spec.Containers = append(ss.Spec.Template.Spec.Containers, exporter) - } - - return ss -} - -func createRedisToolInitContainer(rf *v1.RedisFailover) corev1.Container { - image := config.GetRedisToolsImage() - initContainer := corev1.Container{ - Name: "redis-tools", - Image: image, - ImagePullPolicy: corev1.PullAlways, - Command: []string{"sh", "-c", "cp /opt/* /mnt/opt/ && chmod 555 /mnt/opt/*"}, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("200m"), - corev1.ResourceMemory: resource.MustParse("200Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("200m"), - corev1.ResourceMemory: resource.MustParse("200Mi"), - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: redisOptName, - MountPath: "/mnt/opt/", - }, - }, - } - - return initContainer -} - -func createExposeContainer(rf *v1.RedisFailover) corev1.Container { - image := rf.Spec.Expose.ExposeImage - if image == "" { - image = config.GetRedisToolsImage() - } - privileged := false - container := corev1.Container{ - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, - }, - Name: "expose-pod", - Image: image, - ImagePullPolicy: pullPolicy(rf.Spec.Redis.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: getRedisDataVolumeName(rf), - MountPath: "/data", - }, - }, - Env: []corev1.EnvVar{ - { - Name: "POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "POD_NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - }, { - Name: "SENTINEL_ANNOUNCE_PATH", - Value: "/data/announce.conf", - }, - { - Name: "NODEPORT_ENABLED", - Value: strconv.FormatBool(rf.Spec.Expose.EnableNodePort), - }, - { - Name: "IP_FAMILY", - Value: string(rf.Spec.Redis.IPFamilyPrefer), - }, - }, - Command: []string{"/expose_pod"}, - SecurityContext: &corev1.SecurityContext{Privileged: &privileged}, - } - return container -} - -func createRedisExporterContainer(rf *v1.RedisFailover, secret string) corev1.Container { - container := corev1.Container{ - Name: exporterContainerName, - Image: rf.Spec.Redis.Exporter.Image, - ImagePullPolicy: pullPolicy(rf.Spec.Redis.Exporter.ImagePullPolicy), - Env: []corev1.EnvVar{ - { - Name: "REDIS_ALIAS", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - }, - Ports: []corev1.ContainerPort{ - { - Name: "metrics", - ContainerPort: 9121, - Protocol: corev1.ProtocolTCP, - }, - }, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("200Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("50m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, - }, - SecurityContext: &corev1.SecurityContext{ - ReadOnlyRootFilesystem: pointer.Bool(true), - }, - } - if rf.Spec.Auth.SecretPath != "" || secret != "" { - //挂载 rf.Spec.Auth.SecretPath 到account - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: redisAuthName, - MountPath: "/account", - }) - } - if secret != "" { - container.Env = append(container.Env, corev1.EnvVar{ - Name: "REDIS_USER", - Value: "operator", - }, - ) - } - local_host := "127.0.0.1" - if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - local_host = "[::1]" - } - if rf.Spec.EnableTLS { - container.VolumeMounts = []corev1.VolumeMount{ - { - Name: RedisTLSVolumeName, - MountPath: "/tls", - }, - } - container.Env = append(container.Env, []corev1.EnvVar{ - { - Name: "REDIS_EXPORTER_TLS_CLIENT_KEY_FILE", - Value: "/tls/tls.key", - }, - { - Name: "REDIS_EXPORTER_TLS_CLIENT_CERT_FILE", - Value: "/tls/tls.crt", - }, - { - Name: "REDIS_EXPORTER_TLS_CA_CERT_FILE", - Value: "/tls/ca.crt", - }, - { - Name: "REDIS_EXPORTER_SKIP_TLS_VERIFICATION", - Value: "true", - }, - { - Name: "REDIS_ADDR", - Value: fmt.Sprintf("redis://%s:6379", local_host), - }, - }...) - } else if rf.Spec.Redis.IPFamilyPrefer == corev1.IPv6Protocol { - container.Env = append(container.Env, []corev1.EnvVar{ - {Name: "REDIS_ADDR", - Value: fmt.Sprintf("redis://%s:6379", local_host)}, - }...) - } - - return container -} - -func createAutoReplicaContainer(rf *v1.RedisFailover, secret string) corev1.Container { - image := rf.Spec.Redis.Image - privileged := false - container := corev1.Container{ - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("100Mi"), - }, - }, - Name: "auto-replica", - Image: image, - ImagePullPolicy: pullPolicy(rf.Spec.Redis.ImagePullPolicy), - VolumeMounts: getRedisVolumeMounts(rf, secret), - Command: []string{"sh", "/redis-shutdown/auto_replica.sh"}, - SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - }, - Env: []corev1.EnvVar{ - { - Name: util.GetEnvSentinelHost(rf.Name), - Value: util.GetSentinelName(rf), - }, - { - Name: util.GetEnvSentinelPort(rf.Name), - Value: "26379", - }, - }, - } - return container -} - -func getSecurityContext(secctx *corev1.PodSecurityContext) *corev1.PodSecurityContext { - // 999 is the default userid for redis offical docker image - // 1000 is the default groupid for redis offical docker image - userId, groupId := int64(999), int64(1000) - if secctx == nil { - secctx = &corev1.PodSecurityContext{ - RunAsUser: &userId, - RunAsGroup: &groupId, - FSGroup: &groupId, - RunAsNonRoot: pointer.Bool(true), - } - } else { - if secctx.RunAsUser == nil { - secctx.RunAsUser = &userId - } - if secctx.RunAsGroup == nil { - secctx.RunAsGroup = &groupId - } - if secctx.FSGroup == nil { - secctx.FSGroup = &groupId - } - if *secctx.RunAsUser != 0 { - if secctx.RunAsNonRoot == nil { - secctx.RunAsNonRoot = pointer.Bool(true) - } - } else { - secctx.RunAsNonRoot = nil - } - } - return secctx -} - -func getRedisCommand(rf *v1.RedisFailover) []string { - cmds := []string{ - "sh", - "/redis/entrypoint.sh", - "--tcp-keepalive 60", - } - if rf.Spec.EnableTLS { - cmds = append(cmds, getRedisTLSCommand()...) - } - return cmds -} - -func getAffinity(affinity *corev1.Affinity, labels map[string]string) *corev1.Affinity { - if affinity != nil { - return affinity - } - - // Return a SOFT anti-affinity - return &corev1.Affinity{ - PodAntiAffinity: &corev1.PodAntiAffinity{ - PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ - { - Weight: 100, - PodAffinityTerm: corev1.PodAffinityTerm{ - TopologyKey: util.HostnameTopologyKey, - LabelSelector: &metav1.LabelSelector{ - MatchLabels: labels, - }, - }, - }, - }, - }, - } -} - -func getRedisTLSCommand() []string { - return []string{ - "--tls-port", - "6379", - "--port", - "0", - "--tls-replication", - "yes", - "--tls-cert-file", - "/tls/tls.crt", - "--tls-key-file", - "/tls/tls.key", - "--tls-ca-cert-file", - "/tls/ca.crt", - } -} - -func getRedisVolumeMounts(rf *v1.RedisFailover, secret string) []corev1.VolumeMount { - volumeMounts := []corev1.VolumeMount{ - { - Name: redisConfigurationVolumeName, - MountPath: "/redis", - }, - { - Name: redisShutdownConfigurationVolumeName, - MountPath: "/redis-shutdown", - }, - { - Name: getRedisDataVolumeName(rf), - MountPath: "/data", - }, - { - Name: RedisTmpVolumeName, - MountPath: "/tmp", - }, - } - if rf.Spec.EnableTLS { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: RedisTLSVolumeName, - MountPath: "/tls", - }) - } - if rf.Spec.Auth.SecretPath != "" || secret != "" { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: redisAuthName, - MountPath: "/account", - }) - } - if secret != "" { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: redisOptName, - MountPath: "/opt", - }) - } - - return volumeMounts -} - -func getRedisDataVolumeName(rf *v1.RedisFailover) string { - switch { - case rf.Spec.Redis.Storage.PersistentVolumeClaim != nil: - if rf.Spec.Redis.Storage.PersistentVolumeClaim.ObjectMeta.Name == "" { - return redisStorageVolumeName - } - return rf.Spec.Redis.Storage.PersistentVolumeClaim.ObjectMeta.Name - default: - return redisStorageVolumeName - } -} - -func pullPolicy(specPolicy corev1.PullPolicy) corev1.PullPolicy { - if specPolicy == "" { - return corev1.PullAlways - } - return specPolicy -} - -func getRedisVolumes(rf *v1.RedisFailover, secretName string) []corev1.Volume { - shutdownConfigMapName := util.GetRedisShutdownConfigMapName(rf) - - executeMode := int32(0744) - configname := GetRedisConfigMapName(rf) - volumes := []corev1.Volume{ - { - Name: redisShutdownConfigurationVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: shutdownConfigMapName, - }, - DefaultMode: &executeMode, - }, - }, - }, - { - Name: redisConfigurationVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: configname, - }, - DefaultMode: &executeMode, - }, - }, - }, - { - Name: RedisTmpVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{ - Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(1<<20, resource.BinarySI), //1Mi - }, - }, - }, - } - - dataVolume := getRedisDataVolume(rf) - if dataVolume != nil { - volumes = append(volumes, *dataVolume) - } - if rf.Spec.EnableTLS { - volumes = append(volumes, corev1.Volume{ - Name: RedisTLSVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: util.GetRedisSSLSecretName(rf.Name), - }, - }, - }) - } - if rf.Spec.Auth.SecretPath != "" || secretName != "" { - volumes = append(volumes, corev1.Volume{ - Name: redisAuthName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, - }, - }, - }) - volumes = append(volumes, corev1.Volume{ - Name: redisOptName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }) - } - - return volumes -} - -func getRedisDataVolume(rf *v1.RedisFailover) *corev1.Volume { - // This will find the volumed desired by the user. If no volume defined - // an EmptyDir will be used by default - switch { - case rf.Spec.Redis.Storage.PersistentVolumeClaim != nil: - return nil - default: - return &corev1.Volume{ - Name: redisStorageVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - } - } -} - -func createRestoreContainer(rf *v1.RedisFailover) corev1.Container { - image := config.GetDefaultBackupImage() - if rf.Spec.Redis.Restore.Image != "" { - image = rf.Spec.Redis.Restore.Image - } - container := corev1.Container{ - Name: util.RestoreContainerName, - Image: image, - ImagePullPolicy: pullPolicy(rf.Spec.Redis.Restore.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: util.RedisBackupVolumeName, - MountPath: "/backup", - }, - { - Name: getRedisDataVolumeName(rf), - MountPath: "/data", - }, - }, - Command: []string{"/bin/sh"}, - Args: []string{"-c", "/opt/redis-tools backup restore"}, - } - return container -} - -func createRestoreContainerForS3(rf *v1.RedisFailover, rb *redisbackup.RedisBackup) corev1.Container { - image := rb.Spec.Image - // 不使用redis-backup 恢复镜像 - if strings.Contains(image, "redis-backup:") { - image = os.Getenv(config.GetDefaultBackupImage()) - } - container := corev1.Container{ - Name: util.RestoreContainerName, - Image: image, - ImagePullPolicy: pullPolicy(rf.Spec.Redis.Restore.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: getRedisDataVolumeName(rf), - MountPath: "/data", - }, - { - Name: util.S3SecretVolumeName, - MountPath: "/s3_secret", - ReadOnly: true, - }, - }, - Env: []corev1.EnvVar{ - {Name: "RDB_CHECK", Value: "true"}, - {Name: "TARGET_FILE", Value: "/data/dump.rdb"}, - {Name: "S3_ENDPOINT", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: rb.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_ENDPOINTURL, - }, - }, - }, - {Name: "S3_REGION", ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: rb.Spec.Target.S3Option.S3Secret, - }, - Key: config.S3_REGION, - }, - }}, - {Name: "S3_BUCKET_NAME", Value: rb.Spec.Target.S3Option.Bucket}, - {Name: "S3_OBJECT_NAME", Value: path.Join(rb.Spec.Target.S3Option.Dir, "dump.rdb")}, - }, - Command: []string{"/opt/redis-tools", "backup", "pull"}, - } - return container -} diff --git a/pkg/kubernetes/clientset/cert.go b/pkg/kubernetes/clientset/cert.go index ffbf443..c9e3b9b 100644 --- a/pkg/kubernetes/clientset/cert.go +++ b/pkg/kubernetes/clientset/cert.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -61,21 +61,17 @@ func (c *CertOption) CreateCertificate(ctx context.Context, namespace string, ce if err != nil { return err } - c.logger.WithValues("namespace", namespace, "cert", cert.Name).Info("cert created") + c.logger.WithValues("namespace", namespace, "cert", cert.Name).V(3).Info("cert created") return err } func (c *CertOption) CreateIfNotExistsCertificate(ctx context.Context, namespace string, cert *certv1.Certificate) error { - err := c.client.Get(ctx, types.NamespacedName{ - Name: cert.Name, - Namespace: cert.Namespace, - }, cert) - if err != nil { - if errors.IsNotFound(err) { - err = c.CreateCertificate(ctx, namespace, cert) - return err - } + err := c.client.Get(ctx, types.NamespacedName{Name: cert.Name, Namespace: cert.Namespace}, cert) + if errors.IsNotFound(err) { + err = c.CreateCertificate(ctx, namespace, cert) + return err + } else if err != nil { return err } return nil diff --git a/pkg/kubernetes/clientset/clientset.go b/pkg/kubernetes/clientset/clientset.go index d3990b0..6a4a17b 100644 --- a/pkg/kubernetes/clientset/clientset.go +++ b/pkg/kubernetes/clientset/clientset.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -41,7 +41,7 @@ type ClientSet interface { ServiceMonitor RedisFailover - RedisBackup + RedisSentinel DistributedRedisCluster Node RedisUser @@ -65,10 +65,8 @@ type clientSet struct { StatefulSet ServiceMonitor - RedisBackup - RedisClusterBackup - RedisFailover + RedisSentinel DistributedRedisCluster Node RedisUser @@ -104,9 +102,8 @@ func NewWithConfig(kubecli client.Client, restConfig *rest.Config, logger logr.L ServiceAccount: NewServiceAccount(kubecli, logger), StatefulSet: NewStatefulSet(kubecli, logger), - RedisBackup: NewRedisBackup(kubecli, logger), RedisFailover: NewRedisFailoverService(kubecli, logger), - RedisClusterBackup: NewRedisClusterBackup(kubecli, logger), + RedisSentinel: NewRedisSentinelService(kubecli, logger), DistributedRedisCluster: NewDistributedRedisCluster(kubecli, logger), Node: NewNode(kubecli, logger), ServiceMonitor: NewServiceMonitor(kubecli, logger), diff --git a/pkg/kubernetes/clientset/configmap.go b/pkg/kubernetes/clientset/configmap.go index 417b8eb..226fc4f 100644 --- a/pkg/kubernetes/clientset/configmap.go +++ b/pkg/kubernetes/clientset/configmap.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -24,6 +24,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -80,18 +81,20 @@ func (p *ConfigMapOption) CreateConfigMap(ctx context.Context, namespace string, if err != nil { return err } - p.logger.WithValues("namespace", namespace, "configMap", configMap.Name).Info("configMap created") + p.logger.WithValues("namespace", namespace, "configMap", configMap.Name).V(3).Info("configMap created") return nil } // UpdateConfigMap implement the ConfigMap.Interface func (p *ConfigMapOption) UpdateConfigMap(ctx context.Context, namespace string, configMap *corev1.ConfigMap) error { - err := p.client.Update(ctx, configMap) - if err != nil { - return err - } - p.logger.WithValues("namespace", namespace, "configMap", configMap.Name).Info("configMap updated") - return nil + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldCm, err := p.GetConfigMap(ctx, namespace, configMap.Name) + if err != nil { + return err + } + configMap.ResourceVersion = oldCm.ResourceVersion + return p.client.Update(ctx, configMap) + }) } // CreateIfNotExistsConfigMap implement the ConfigMap.Interface @@ -153,7 +156,6 @@ func (p *ConfigMapOption) UpdateIfConfigMapChanged(ctx context.Context, newConfi return err } if !reflect.DeepEqual(newConfigmap.Data, oldConfigmap.Data) { - p.logger.Info("configmap changed, update it", "name", newConfigmap.Name) return p.UpdateConfigMap(ctx, newConfigmap.Namespace, newConfigmap) } return nil diff --git a/pkg/kubernetes/clientset/cronjob.go b/pkg/kubernetes/clientset/cronjob.go index 9e92f01..bc13747 100644 --- a/pkg/kubernetes/clientset/cronjob.go +++ b/pkg/kubernetes/clientset/cronjob.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -83,7 +83,7 @@ func (c *CronJobOption) CreateCronJob(ctx context.Context, namespace string, cro if err != nil { return err } - c.logger.WithValues("namespace", namespace, "cronjob", cronjob.ObjectMeta.Name).Info("cronjob created") + c.logger.WithValues("namespace", namespace, "cronjob", cronjob.ObjectMeta.Name).V(3).Info("cronjob created") return err } @@ -92,7 +92,7 @@ func (c *CronJobOption) UpdateCronJob(ctx context.Context, namespace string, cro if err != nil { return err } - c.logger.WithValues("namespace", namespace, "cronjob", cronjob.ObjectMeta.Name).Info("cronjob updated") + c.logger.WithValues("namespace", namespace, "cronjob", cronjob.ObjectMeta.Name).V(3).Info("cronjob updated") return nil } diff --git a/pkg/kubernetes/clientset/deployment.go b/pkg/kubernetes/clientset/deployment.go index 5ed1292..f3f52de 100644 --- a/pkg/kubernetes/clientset/deployment.go +++ b/pkg/kubernetes/clientset/deployment.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -142,7 +143,7 @@ func (d *DeploymentOption) CreateDeployment(ctx context.Context, namespace strin if err != nil { return err } - d.logger.WithValues("namespace", namespace, "deployment", deployment.ObjectMeta.Name).Info("deployment created") + d.logger.WithValues("namespace", namespace, "deployment", deployment.ObjectMeta.Name).V(3).Info("deployment created") return err } @@ -152,27 +153,23 @@ func (d *DeploymentOption) UpdateDeployment(ctx context.Context, namespace strin if err != nil { return err } - d.logger.WithValues("namespace", namespace, "deployment", deployment.ObjectMeta.Name).Info("deployment updated") + d.logger.WithValues("namespace", namespace, "deployment", deployment.ObjectMeta.Name).V(3).Info("deployment updated") return err } // CreateOrUpdateDeployment implement the Deployment.Interface func (d *DeploymentOption) CreateOrUpdateDeployment(ctx context.Context, namespace string, deployment *appsv1.Deployment) error { - storedDeployment, err := d.GetDeployment(ctx, namespace, deployment.Name) - if err != nil { - // If no resource we need to create. - if errors.IsNotFound(err) { - return d.CreateDeployment(ctx, namespace, deployment) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + storedDeployment, err := d.GetDeployment(ctx, namespace, deployment.Name) + if err != nil { + if errors.IsNotFound(err) { + return d.CreateDeployment(ctx, namespace, deployment) + } + return err } - return err - } - - // Already exists, need to Update. - // Set the correct resource version to ensure we are on the latest version. This way the only valid - // namespace is our spec(https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency), - // we will replace the current namespace state. - deployment.ResourceVersion = storedDeployment.ResourceVersion - return d.UpdateDeployment(ctx, namespace, deployment) + deployment.ResourceVersion = storedDeployment.ResourceVersion + return d.UpdateDeployment(ctx, namespace, deployment) + }) } // DeleteDeployment implement the Deployment.Interface diff --git a/pkg/kubernetes/clientset/distributedrediscluster.go b/pkg/kubernetes/clientset/distributedrediscluster.go index a316e9d..35b23e3 100644 --- a/pkg/kubernetes/clientset/distributedrediscluster.go +++ b/pkg/kubernetes/clientset/distributedrediscluster.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,15 +18,21 @@ package clientset import ( "context" + "reflect" - clusterv1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" ) type DistributedRedisCluster interface { GetDistributedRedisCluster(ctx context.Context, namespace, name string) (*clusterv1.DistributedRedisCluster, error) + // UpdateRedisFailover update the redisfailover on a cluster. + UpdateDistributedRedisCluster(ctx context.Context, inst *clusterv1.DistributedRedisCluster) error + // UpdateRedisFailoverStatus + UpdateDistributedRedisClusterStatus(ctx context.Context, inst *clusterv1.DistributedRedisCluster) error } type DistributedRedisClusterOption struct { @@ -35,7 +41,7 @@ type DistributedRedisClusterOption struct { } func NewDistributedRedisCluster(kubeClient client.Client, logger logr.Logger) DistributedRedisCluster { - logger = logger.WithValues("service", "k8s.deployment") + logger = logger.WithName("DistributedRedisCluster") return &DistributedRedisClusterOption{ client: kubeClient, logger: logger, @@ -53,3 +59,32 @@ func (r *DistributedRedisClusterOption) GetDistributedRedisCluster(ctx context.C } return &ret, nil } + +// UpdateRedisFailover +func (r *DistributedRedisClusterOption) UpdateDistributedRedisCluster(ctx context.Context, inst *clusterv1.DistributedRedisCluster) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst clusterv1.DistributedRedisCluster + if err := r.client.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + r.logger.Error(err, "get DistributedRedisCluster failed") + return err + } + inst.ResourceVersion = oldInst.ResourceVersion + return r.client.Update(ctx, inst) + }) +} + +func (r *DistributedRedisClusterOption) UpdateDistributedRedisClusterStatus(ctx context.Context, inst *clusterv1.DistributedRedisCluster) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst clusterv1.DistributedRedisCluster + if err := r.client.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + r.logger.Error(err, "get DistributedRedisCluster failed") + return err + } + + if !reflect.DeepEqual(oldInst.Status, inst.Status) { + inst.ResourceVersion = oldInst.ResourceVersion + return r.client.Status().Update(ctx, inst) + } + return nil + }) +} diff --git a/pkg/kubernetes/clientset/job.go b/pkg/kubernetes/clientset/job.go index 1e17573..a031260 100644 --- a/pkg/kubernetes/clientset/job.go +++ b/pkg/kubernetes/clientset/job.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -86,7 +86,7 @@ func (c *JobOption) CreateJob(ctx context.Context, namespace string, job *v1.Job if err != nil { return err } - c.logger.WithValues("namespace", namespace, "Job", job.ObjectMeta.Name).Info("Job created") + c.logger.WithValues("namespace", namespace, "Job", job.ObjectMeta.Name).V(3).Info("Job created") return err } @@ -95,7 +95,7 @@ func (c *JobOption) UpdateJob(ctx context.Context, namespace string, job *v1.Job if err != nil { return err } - c.logger.WithValues("namespace", namespace, "Job", job.ObjectMeta.Name).Info("Job updated") + c.logger.WithValues("namespace", namespace, "Job", job.ObjectMeta.Name).V(3).Info("Job updated") return nil } diff --git a/pkg/kubernetes/clientset/mocks/certificate.go b/pkg/kubernetes/clientset/mocks/certificate.go new file mode 100644 index 0000000..2f0ec14 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/certificate.go @@ -0,0 +1,111 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + mock "github.com/stretchr/testify/mock" +) + +// Certificate is an autogenerated mock type for the Certificate type +type Certificate struct { + mock.Mock +} + +// CreateCertificate provides a mock function with given fields: ctx, namespace, cert +func (_m *Certificate) CreateCertificate(ctx context.Context, namespace string, cert *v1.Certificate) error { + ret := _m.Called(ctx, namespace, cert) + + if len(ret) == 0 { + panic("no return value specified for CreateCertificate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Certificate) error); ok { + r0 = rf(ctx, namespace, cert) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsCertificate provides a mock function with given fields: ctx, namespace, cert +func (_m *Certificate) CreateIfNotExistsCertificate(ctx context.Context, namespace string, cert *v1.Certificate) error { + ret := _m.Called(ctx, namespace, cert) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsCertificate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Certificate) error); ok { + r0 = rf(ctx, namespace, cert) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetCertificate provides a mock function with given fields: ctx, namespace, name +func (_m *Certificate) GetCertificate(ctx context.Context, namespace string, name string) (*v1.Certificate, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetCertificate") + } + + var r0 *v1.Certificate + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Certificate, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Certificate); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Certificate) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewCertificate creates a new instance of Certificate. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCertificate(t interface { + mock.TestingT + Cleanup(func()) +}) *Certificate { + mock := &Certificate{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/client_set.go b/pkg/kubernetes/clientset/mocks/client_set.go new file mode 100644 index 0000000..3da6bfe --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/client_set.go @@ -0,0 +1,2865 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + appsv1 "k8s.io/api/apps/v1" + + batchv1 "k8s.io/api/batch/v1" + + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + corev1 "k8s.io/api/core/v1" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + + io "io" + + mock "github.com/stretchr/testify/mock" + + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + + policyv1 "k8s.io/api/policy/v1" + + rbacv1 "k8s.io/api/rbac/v1" + + redisv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" + + v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + + v1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" +) + +// ClientSet is an autogenerated mock type for the ClientSet type +type ClientSet struct { + mock.Mock +} + +// Client provides a mock function with given fields: +func (_m *ClientSet) Client() client.Client { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Client") + } + + var r0 client.Client + if rf, ok := ret.Get(0).(func() client.Client); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(client.Client) + } + } + + return r0 +} + +// CreateCertificate provides a mock function with given fields: ctx, namespace, cert +func (_m *ClientSet) CreateCertificate(ctx context.Context, namespace string, cert *v1.Certificate) error { + ret := _m.Called(ctx, namespace, cert) + + if len(ret) == 0 { + panic("no return value specified for CreateCertificate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Certificate) error); ok { + r0 = rf(ctx, namespace, cert) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateClusterRole provides a mock function with given fields: ctx, role +func (_m *ClientSet) CreateClusterRole(ctx context.Context, role *rbacv1.ClusterRole) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for CreateClusterRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *rbacv1.ClusterRole) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateClusterRoleBinding provides a mock function with given fields: ctx, rb +func (_m *ClientSet) CreateClusterRoleBinding(ctx context.Context, rb *rbacv1.ClusterRoleBinding) error { + ret := _m.Called(ctx, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateClusterRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *rbacv1.ClusterRoleBinding) error); ok { + r0 = rf(ctx, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateConfigMap provides a mock function with given fields: ctx, namespace, configMap +func (_m *ClientSet) CreateConfigMap(ctx context.Context, namespace string, configMap *corev1.ConfigMap) error { + ret := _m.Called(ctx, namespace, configMap) + + if len(ret) == 0 { + panic("no return value specified for CreateConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, configMap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateCronJob provides a mock function with given fields: ctx, namespace, cronjob +func (_m *ClientSet) CreateCronJob(ctx context.Context, namespace string, cronjob *batchv1.CronJob) error { + ret := _m.Called(ctx, namespace, cronjob) + + if len(ret) == 0 { + panic("no return value specified for CreateCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *batchv1.CronJob) error); ok { + r0 = rf(ctx, namespace, cronjob) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateDeployment provides a mock function with given fields: ctx, namespace, deployment +func (_m *ClientSet) CreateDeployment(ctx context.Context, namespace string, deployment *appsv1.Deployment) error { + ret := _m.Called(ctx, namespace, deployment) + + if len(ret) == 0 { + panic("no return value specified for CreateDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.Deployment) error); ok { + r0 = rf(ctx, namespace, deployment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsCertificate provides a mock function with given fields: ctx, namespace, cert +func (_m *ClientSet) CreateIfNotExistsCertificate(ctx context.Context, namespace string, cert *v1.Certificate) error { + ret := _m.Called(ctx, namespace, cert) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsCertificate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Certificate) error); ok { + r0 = rf(ctx, namespace, cert) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsConfigMap provides a mock function with given fields: ctx, namespace, configMap +func (_m *ClientSet) CreateIfNotExistsConfigMap(ctx context.Context, namespace string, configMap *corev1.ConfigMap) error { + ret := _m.Called(ctx, namespace, configMap) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, configMap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsDeployment provides a mock function with given fields: ctx, namespace, deploy +func (_m *ClientSet) CreateIfNotExistsDeployment(ctx context.Context, namespace string, deploy *appsv1.Deployment) error { + ret := _m.Called(ctx, namespace, deploy) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.Deployment) error); ok { + r0 = rf(ctx, namespace, deploy) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsJob provides a mock function with given fields: ctx, namespace, job +func (_m *ClientSet) CreateIfNotExistsJob(ctx context.Context, namespace string, job *batchv1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *batchv1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsPodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *ClientSet) CreateIfNotExistsPodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsPodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *policyv1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsRedisUser provides a mock function with given fields: ctx, ru +func (_m *ClientSet) CreateIfNotExistsRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *redisv1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *ClientSet) CreateIfNotExistsSecret(ctx context.Context, namespace string, secret *corev1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsService provides a mock function with given fields: ctx, namespace, service +func (_m *ClientSet) CreateIfNotExistsService(ctx context.Context, namespace string, service *corev1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsStatefulSet provides a mock function with given fields: ctx, namespace, statefulSet +func (_m *ClientSet) CreateIfNotExistsStatefulSet(ctx context.Context, namespace string, statefulSet *appsv1.StatefulSet) error { + ret := _m.Called(ctx, namespace, statefulSet) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, statefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateJob provides a mock function with given fields: ctx, namespace, job +func (_m *ClientSet) CreateJob(ctx context.Context, namespace string, job *batchv1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for CreateJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *batchv1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateClusterRole provides a mock function with given fields: ctx, role +func (_m *ClientSet) CreateOrUpdateClusterRole(ctx context.Context, role *rbacv1.ClusterRole) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateClusterRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *rbacv1.ClusterRole) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateClusterRoleBinding provides a mock function with given fields: ctx, rb +func (_m *ClientSet) CreateOrUpdateClusterRoleBinding(ctx context.Context, rb *rbacv1.ClusterRoleBinding) error { + ret := _m.Called(ctx, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateClusterRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *rbacv1.ClusterRoleBinding) error); ok { + r0 = rf(ctx, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateConfigMap provides a mock function with given fields: ctx, namespace, np +func (_m *ClientSet) CreateOrUpdateConfigMap(ctx context.Context, namespace string, np *corev1.ConfigMap) error { + ret := _m.Called(ctx, namespace, np) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, np) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateCronJob provides a mock function with given fields: ctx, namespace, cronjob +func (_m *ClientSet) CreateOrUpdateCronJob(ctx context.Context, namespace string, cronjob *batchv1.CronJob) error { + ret := _m.Called(ctx, namespace, cronjob) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *batchv1.CronJob) error); ok { + r0 = rf(ctx, namespace, cronjob) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateDeployment provides a mock function with given fields: ctx, namespace, deployment +func (_m *ClientSet) CreateOrUpdateDeployment(ctx context.Context, namespace string, deployment *appsv1.Deployment) error { + ret := _m.Called(ctx, namespace, deployment) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.Deployment) error); ok { + r0 = rf(ctx, namespace, deployment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateIfServiceChanged provides a mock function with given fields: ctx, namespace, service +func (_m *ClientSet) CreateOrUpdateIfServiceChanged(ctx context.Context, namespace string, service *corev1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateIfServiceChanged") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateJob provides a mock function with given fields: ctx, namespace, job +func (_m *ClientSet) CreateOrUpdateJob(ctx context.Context, namespace string, job *batchv1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *batchv1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdatePod provides a mock function with given fields: ctx, namespace, pod +func (_m *ClientSet) CreateOrUpdatePod(ctx context.Context, namespace string, pod *corev1.Pod) error { + ret := _m.Called(ctx, namespace, pod) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdatePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Pod) error); ok { + r0 = rf(ctx, namespace, pod) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdatePodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *ClientSet) CreateOrUpdatePodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdatePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *policyv1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateRedisUser provides a mock function with given fields: ctx, ru +func (_m *ClientSet) CreateOrUpdateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *redisv1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateRole provides a mock function with given fields: ctx, namespace, role +func (_m *ClientSet) CreateOrUpdateRole(ctx context.Context, namespace string, role *rbacv1.Role) error { + ret := _m.Called(ctx, namespace, role) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *rbacv1.Role) error); ok { + r0 = rf(ctx, namespace, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateRoleBinding provides a mock function with given fields: ctx, namespace, rb +func (_m *ClientSet) CreateOrUpdateRoleBinding(ctx context.Context, namespace string, rb *rbacv1.RoleBinding) error { + ret := _m.Called(ctx, namespace, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *rbacv1.RoleBinding) error); ok { + r0 = rf(ctx, namespace, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *ClientSet) CreateOrUpdateSecret(ctx context.Context, namespace string, secret *corev1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateService provides a mock function with given fields: ctx, namespace, service +func (_m *ClientSet) CreateOrUpdateService(ctx context.Context, namespace string, service *corev1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateServiceAccount provides a mock function with given fields: ctx, namespace, sa +func (_m *ClientSet) CreateOrUpdateServiceAccount(ctx context.Context, namespace string, sa *corev1.ServiceAccount) error { + ret := _m.Called(ctx, namespace, sa) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateServiceAccount") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.ServiceAccount) error); ok { + r0 = rf(ctx, namespace, sa) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateServiceMonitor provides a mock function with given fields: ctx, namespace, sm +func (_m *ClientSet) CreateOrUpdateServiceMonitor(ctx context.Context, namespace string, sm *monitoringv1.ServiceMonitor) error { + ret := _m.Called(ctx, namespace, sm) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateServiceMonitor") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *monitoringv1.ServiceMonitor) error); ok { + r0 = rf(ctx, namespace, sm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateStatefulSet provides a mock function with given fields: ctx, namespace, StatefulSet +func (_m *ClientSet) CreateOrUpdateStatefulSet(ctx context.Context, namespace string, StatefulSet *appsv1.StatefulSet) error { + ret := _m.Called(ctx, namespace, StatefulSet) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, StatefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreatePVC provides a mock function with given fields: ctx, namespace, pvc +func (_m *ClientSet) CreatePVC(ctx context.Context, namespace string, pvc *corev1.PersistentVolumeClaim) error { + ret := _m.Called(ctx, namespace, pvc) + + if len(ret) == 0 { + panic("no return value specified for CreatePVC") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.PersistentVolumeClaim) error); ok { + r0 = rf(ctx, namespace, pvc) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreatePod provides a mock function with given fields: ctx, namespace, pod +func (_m *ClientSet) CreatePod(ctx context.Context, namespace string, pod *corev1.Pod) error { + ret := _m.Called(ctx, namespace, pod) + + if len(ret) == 0 { + panic("no return value specified for CreatePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Pod) error); ok { + r0 = rf(ctx, namespace, pod) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreatePodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *ClientSet) CreatePodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for CreatePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *policyv1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateRedisUser provides a mock function with given fields: ctx, ru +func (_m *ClientSet) CreateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for CreateRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *redisv1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateRole provides a mock function with given fields: ctx, namespace, role +func (_m *ClientSet) CreateRole(ctx context.Context, namespace string, role *rbacv1.Role) error { + ret := _m.Called(ctx, namespace, role) + + if len(ret) == 0 { + panic("no return value specified for CreateRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *rbacv1.Role) error); ok { + r0 = rf(ctx, namespace, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateRoleBinding provides a mock function with given fields: ctx, namespace, rb +func (_m *ClientSet) CreateRoleBinding(ctx context.Context, namespace string, rb *rbacv1.RoleBinding) error { + ret := _m.Called(ctx, namespace, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *rbacv1.RoleBinding) error); ok { + r0 = rf(ctx, namespace, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *ClientSet) CreateSecret(ctx context.Context, namespace string, secret *corev1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for CreateSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateService provides a mock function with given fields: ctx, namespace, service +func (_m *ClientSet) CreateService(ctx context.Context, namespace string, service *corev1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateServiceAccount provides a mock function with given fields: ctx, namespace, sa +func (_m *ClientSet) CreateServiceAccount(ctx context.Context, namespace string, sa *corev1.ServiceAccount) error { + ret := _m.Called(ctx, namespace, sa) + + if len(ret) == 0 { + panic("no return value specified for CreateServiceAccount") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.ServiceAccount) error); ok { + r0 = rf(ctx, namespace, sa) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateServiceMonitor provides a mock function with given fields: ctx, namespace, sm +func (_m *ClientSet) CreateServiceMonitor(ctx context.Context, namespace string, sm *monitoringv1.ServiceMonitor) error { + ret := _m.Called(ctx, namespace, sm) + + if len(ret) == 0 { + panic("no return value specified for CreateServiceMonitor") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *monitoringv1.ServiceMonitor) error); ok { + r0 = rf(ctx, namespace, sm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateStatefulSet provides a mock function with given fields: ctx, namespace, statefulSet +func (_m *ClientSet) CreateStatefulSet(ctx context.Context, namespace string, statefulSet *appsv1.StatefulSet) error { + ret := _m.Called(ctx, namespace, statefulSet) + + if len(ret) == 0 { + panic("no return value specified for CreateStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, statefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteConfigMap provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteConfigMap(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteCronJob provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteCronJob(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteDeployment provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteDeployment(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteJob provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteJob(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeletePod provides a mock function with given fields: ctx, namespace, name, opts +func (_m *ClientSet) DeletePod(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, namespace, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeletePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...client.DeleteOption) error); ok { + r0 = rf(ctx, namespace, name, opts...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeletePodDisruptionBudget provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeletePodDisruptionBudget(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeletePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteRedisBackup provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteRedisBackup(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteRedisBackup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteRedisClusterBackup provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteRedisClusterBackup(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteRedisClusterBackup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteSecret provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteSecret(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteService provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) DeleteService(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteStatefulSet provides a mock function with given fields: ctx, namespace, name, opts +func (_m *ClientSet) DeleteStatefulSet(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, namespace, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...client.DeleteOption) error); ok { + r0 = rf(ctx, namespace, name, opts...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Exec provides a mock function with given fields: ctx, namespace, name, containerName, cmd +func (_m *ClientSet) Exec(ctx context.Context, namespace string, name string, containerName string, cmd []string) (io.Reader, io.Reader, error) { + ret := _m.Called(ctx, namespace, name, containerName, cmd) + + if len(ret) == 0 { + panic("no return value specified for Exec") + } + + var r0 io.Reader + var r1 io.Reader + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, []string) (io.Reader, io.Reader, error)); ok { + return rf(ctx, namespace, name, containerName, cmd) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, []string) io.Reader); ok { + r0 = rf(ctx, namespace, name, containerName, cmd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.Reader) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, []string) io.Reader); ok { + r1 = rf(ctx, namespace, name, containerName, cmd) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(io.Reader) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, string, []string) error); ok { + r2 = rf(ctx, namespace, name, containerName, cmd) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetCertificate provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetCertificate(ctx context.Context, namespace string, name string) (*v1.Certificate, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetCertificate") + } + + var r0 *v1.Certificate + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Certificate, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Certificate); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Certificate) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetClusterRole provides a mock function with given fields: ctx, name +func (_m *ClientSet) GetClusterRole(ctx context.Context, name string) (*rbacv1.ClusterRole, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetClusterRole") + } + + var r0 *rbacv1.ClusterRole + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*rbacv1.ClusterRole, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *rbacv1.ClusterRole); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rbacv1.ClusterRole) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetClusterRoleBinding provides a mock function with given fields: ctx, name +func (_m *ClientSet) GetClusterRoleBinding(ctx context.Context, name string) (*rbacv1.ClusterRoleBinding, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetClusterRoleBinding") + } + + var r0 *rbacv1.ClusterRoleBinding + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*rbacv1.ClusterRoleBinding, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *rbacv1.ClusterRoleBinding); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rbacv1.ClusterRoleBinding) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetConfigMap provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetConfigMap(ctx context.Context, namespace string, name string) (*corev1.ConfigMap, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetConfigMap") + } + + var r0 *corev1.ConfigMap + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.ConfigMap, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.ConfigMap); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.ConfigMap) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCronJob provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetCronJob(ctx context.Context, namespace string, name string) (*batchv1.CronJob, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetCronJob") + } + + var r0 *batchv1.CronJob + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*batchv1.CronJob, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *batchv1.CronJob); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*batchv1.CronJob) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetDeployment provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetDeployment(ctx context.Context, namespace string, name string) (*appsv1.Deployment, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetDeployment") + } + + var r0 *appsv1.Deployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*appsv1.Deployment, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *appsv1.Deployment); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*appsv1.Deployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetDeploymentPods provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetDeploymentPods(ctx context.Context, namespace string, name string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetDeploymentPods") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.PodList); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetDistributedRedisCluster provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetDistributedRedisCluster(ctx context.Context, namespace string, name string) (*v1alpha1.DistributedRedisCluster, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetDistributedRedisCluster") + } + + var r0 *v1alpha1.DistributedRedisCluster + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1alpha1.DistributedRedisCluster, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1alpha1.DistributedRedisCluster); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1alpha1.DistributedRedisCluster) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetJob provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetJob(ctx context.Context, namespace string, name string) (*batchv1.Job, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetJob") + } + + var r0 *batchv1.Job + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*batchv1.Job, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *batchv1.Job); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*batchv1.Job) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetNameSpace provides a mock function with given fields: ctx, namespace +func (_m *ClientSet) GetNameSpace(ctx context.Context, namespace string) (*corev1.Namespace, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for GetNameSpace") + } + + var r0 *corev1.Namespace + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.Namespace, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.Namespace); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.Namespace) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetNode provides a mock function with given fields: ctx, name +func (_m *ClientSet) GetNode(ctx context.Context, name string) (*corev1.Node, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetNode") + } + + var r0 *corev1.Node + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.Node, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.Node); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.Node) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPVC provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetPVC(ctx context.Context, namespace string, name string) (*corev1.PersistentVolumeClaim, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetPVC") + } + + var r0 *corev1.PersistentVolumeClaim + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.PersistentVolumeClaim, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.PersistentVolumeClaim); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PersistentVolumeClaim) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPod provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetPod(ctx context.Context, namespace string, name string) (*corev1.Pod, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetPod") + } + + var r0 *corev1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.Pod, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.Pod); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.Pod) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPodDisruptionBudget provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetPodDisruptionBudget(ctx context.Context, namespace string, name string) (*policyv1.PodDisruptionBudget, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetPodDisruptionBudget") + } + + var r0 *policyv1.PodDisruptionBudget + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*policyv1.PodDisruptionBudget, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *policyv1.PodDisruptionBudget); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*policyv1.PodDisruptionBudget) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRedisFailover provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetRedisFailover(ctx context.Context, namespace string, name string) (*databasesv1.RedisFailover, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRedisFailover") + } + + var r0 *databasesv1.RedisFailover + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*databasesv1.RedisFailover, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *databasesv1.RedisFailover); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*databasesv1.RedisFailover) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRedisSentinel provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetRedisSentinel(ctx context.Context, namespace string, name string) (*databasesv1.RedisSentinel, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRedisSentinel") + } + + var r0 *databasesv1.RedisSentinel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*databasesv1.RedisSentinel, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *databasesv1.RedisSentinel); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*databasesv1.RedisSentinel) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRedisUser provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetRedisUser(ctx context.Context, namespace string, name string) (*redisv1.RedisUser, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRedisUser") + } + + var r0 *redisv1.RedisUser + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*redisv1.RedisUser, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *redisv1.RedisUser); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redisv1.RedisUser) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRole provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetRole(ctx context.Context, namespace string, name string) (*rbacv1.Role, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRole") + } + + var r0 *rbacv1.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*rbacv1.Role, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *rbacv1.Role); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rbacv1.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRoleBinding provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetRoleBinding(ctx context.Context, namespace string, name string) (*rbacv1.RoleBinding, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRoleBinding") + } + + var r0 *rbacv1.RoleBinding + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*rbacv1.RoleBinding, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *rbacv1.RoleBinding); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rbacv1.RoleBinding) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSecret provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetSecret(ctx context.Context, namespace string, name string) (*corev1.Secret, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetSecret") + } + + var r0 *corev1.Secret + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.Secret, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.Secret); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.Secret) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetService provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetService(ctx context.Context, namespace string, name string) (*corev1.Service, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetService") + } + + var r0 *corev1.Service + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.Service, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.Service); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.Service) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetServiceAccount provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetServiceAccount(ctx context.Context, namespace string, name string) (*corev1.ServiceAccount, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetServiceAccount") + } + + var r0 *corev1.ServiceAccount + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.ServiceAccount, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.ServiceAccount); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.ServiceAccount) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetServiceByLabels provides a mock function with given fields: ctx, namespace, labelsMap +func (_m *ClientSet) GetServiceByLabels(ctx context.Context, namespace string, labelsMap map[string]string) (*corev1.ServiceList, error) { + ret := _m.Called(ctx, namespace, labelsMap) + + if len(ret) == 0 { + panic("no return value specified for GetServiceByLabels") + } + + var r0 *corev1.ServiceList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*corev1.ServiceList, error)); ok { + return rf(ctx, namespace, labelsMap) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *corev1.ServiceList); ok { + r0 = rf(ctx, namespace, labelsMap) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.ServiceList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, labelsMap) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetServiceMonitor provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetServiceMonitor(ctx context.Context, namespace string, name string) (*monitoringv1.ServiceMonitor, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetServiceMonitor") + } + + var r0 *monitoringv1.ServiceMonitor + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*monitoringv1.ServiceMonitor, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *monitoringv1.ServiceMonitor); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*monitoringv1.ServiceMonitor) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStatefulSet provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetStatefulSet(ctx context.Context, namespace string, name string) (*appsv1.StatefulSet, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetStatefulSet") + } + + var r0 *appsv1.StatefulSet + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*appsv1.StatefulSet, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *appsv1.StatefulSet); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*appsv1.StatefulSet) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStatefulSetPods provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) GetStatefulSetPods(ctx context.Context, namespace string, name string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetStatefulSetPods") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.PodList); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStatefulSetPodsByLabels provides a mock function with given fields: ctx, namespace, labels +func (_m *ClientSet) GetStatefulSetPodsByLabels(ctx context.Context, namespace string, labels map[string]string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace, labels) + + if len(ret) == 0 { + panic("no return value specified for GetStatefulSetPodsByLabels") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace, labels) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *corev1.PodList); ok { + r0 = rf(ctx, namespace, labels) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, labels) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListConfigMaps provides a mock function with given fields: ctx, namespace +func (_m *ClientSet) ListConfigMaps(ctx context.Context, namespace string) (*corev1.ConfigMapList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListConfigMaps") + } + + var r0 *corev1.ConfigMapList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.ConfigMapList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.ConfigMapList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.ConfigMapList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListCronJobs provides a mock function with given fields: ctx, namespace, cl +func (_m *ClientSet) ListCronJobs(ctx context.Context, namespace string, cl client.ListOptions) (*batchv1.CronJobList, error) { + ret := _m.Called(ctx, namespace, cl) + + if len(ret) == 0 { + panic("no return value specified for ListCronJobs") + } + + var r0 *batchv1.CronJobList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*batchv1.CronJobList, error)); ok { + return rf(ctx, namespace, cl) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *batchv1.CronJobList); ok { + r0 = rf(ctx, namespace, cl) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*batchv1.CronJobList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, cl) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListDeployments provides a mock function with given fields: ctx, namespace +func (_m *ClientSet) ListDeployments(ctx context.Context, namespace string) (*appsv1.DeploymentList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListDeployments") + } + + var r0 *appsv1.DeploymentList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*appsv1.DeploymentList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *appsv1.DeploymentList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*appsv1.DeploymentList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListJobs provides a mock function with given fields: ctx, namespace, cl +func (_m *ClientSet) ListJobs(ctx context.Context, namespace string, cl client.ListOptions) (*batchv1.JobList, error) { + ret := _m.Called(ctx, namespace, cl) + + if len(ret) == 0 { + panic("no return value specified for ListJobs") + } + + var r0 *batchv1.JobList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*batchv1.JobList, error)); ok { + return rf(ctx, namespace, cl) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *batchv1.JobList); ok { + r0 = rf(ctx, namespace, cl) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*batchv1.JobList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, cl) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListJobsByLabel provides a mock function with given fields: ctx, namespace, label_map +func (_m *ClientSet) ListJobsByLabel(ctx context.Context, namespace string, label_map map[string]string) (*batchv1.JobList, error) { + ret := _m.Called(ctx, namespace, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListJobsByLabel") + } + + var r0 *batchv1.JobList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*batchv1.JobList, error)); ok { + return rf(ctx, namespace, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *batchv1.JobList); ok { + r0 = rf(ctx, namespace, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*batchv1.JobList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListNodesByLabels provides a mock function with given fields: ctx, label_map +func (_m *ClientSet) ListNodesByLabels(ctx context.Context, label_map map[string]string) (*corev1.NodeList, error) { + ret := _m.Called(ctx, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListNodesByLabels") + } + + var r0 *corev1.NodeList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, map[string]string) (*corev1.NodeList, error)); ok { + return rf(ctx, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, map[string]string) *corev1.NodeList); ok { + r0 = rf(ctx, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.NodeList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, map[string]string) error); ok { + r1 = rf(ctx, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListPodByLabels provides a mock function with given fields: ctx, namespace, label_map +func (_m *ClientSet) ListPodByLabels(ctx context.Context, namespace string, label_map map[string]string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListPodByLabels") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *corev1.PodList); ok { + r0 = rf(ctx, namespace, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListPods provides a mock function with given fields: ctx, namespace +func (_m *ClientSet) ListPods(ctx context.Context, namespace string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListPods") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.PodList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListPvcByLabel provides a mock function with given fields: ctx, namespace, label_map +func (_m *ClientSet) ListPvcByLabel(ctx context.Context, namespace string, label_map map[string]string) (*corev1.PersistentVolumeClaimList, error) { + ret := _m.Called(ctx, namespace, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListPvcByLabel") + } + + var r0 *corev1.PersistentVolumeClaimList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*corev1.PersistentVolumeClaimList, error)); ok { + return rf(ctx, namespace, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *corev1.PersistentVolumeClaimList); ok { + r0 = rf(ctx, namespace, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PersistentVolumeClaimList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRedisFailovers provides a mock function with given fields: ctx, namespace, opts +func (_m *ClientSet) ListRedisFailovers(ctx context.Context, namespace string, opts client.ListOptions) (*databasesv1.RedisFailoverList, error) { + ret := _m.Called(ctx, namespace, opts) + + if len(ret) == 0 { + panic("no return value specified for ListRedisFailovers") + } + + var r0 *databasesv1.RedisFailoverList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*databasesv1.RedisFailoverList, error)); ok { + return rf(ctx, namespace, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *databasesv1.RedisFailoverList); ok { + r0 = rf(ctx, namespace, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*databasesv1.RedisFailoverList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRedisSentinels provides a mock function with given fields: ctx, namespace, opts +func (_m *ClientSet) ListRedisSentinels(ctx context.Context, namespace string, opts client.ListOptions) (*databasesv1.RedisSentinelList, error) { + ret := _m.Called(ctx, namespace, opts) + + if len(ret) == 0 { + panic("no return value specified for ListRedisSentinels") + } + + var r0 *databasesv1.RedisSentinelList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*databasesv1.RedisSentinelList, error)); ok { + return rf(ctx, namespace, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *databasesv1.RedisSentinelList); ok { + r0 = rf(ctx, namespace, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*databasesv1.RedisSentinelList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRedisUsers provides a mock function with given fields: ctx, namespace, opts +func (_m *ClientSet) ListRedisUsers(ctx context.Context, namespace string, opts client.ListOptions) (*redisv1.RedisUserList, error) { + ret := _m.Called(ctx, namespace, opts) + + if len(ret) == 0 { + panic("no return value specified for ListRedisUsers") + } + + var r0 *redisv1.RedisUserList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*redisv1.RedisUserList, error)); ok { + return rf(ctx, namespace, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *redisv1.RedisUserList); ok { + r0 = rf(ctx, namespace, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redisv1.RedisUserList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListSecret provides a mock function with given fields: ctx, namespace +func (_m *ClientSet) ListSecret(ctx context.Context, namespace string) (*corev1.SecretList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListSecret") + } + + var r0 *corev1.SecretList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.SecretList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.SecretList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.SecretList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListServices provides a mock function with given fields: ctx, namespace +func (_m *ClientSet) ListServices(ctx context.Context, namespace string) (*corev1.ServiceList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListServices") + } + + var r0 *corev1.ServiceList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.ServiceList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.ServiceList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.ServiceList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStatefulSetByLabels provides a mock function with given fields: ctx, namespace, labels +func (_m *ClientSet) ListStatefulSetByLabels(ctx context.Context, namespace string, labels map[string]string) (*appsv1.StatefulSetList, error) { + ret := _m.Called(ctx, namespace, labels) + + if len(ret) == 0 { + panic("no return value specified for ListStatefulSetByLabels") + } + + var r0 *appsv1.StatefulSetList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*appsv1.StatefulSetList, error)); ok { + return rf(ctx, namespace, labels) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *appsv1.StatefulSetList); ok { + r0 = rf(ctx, namespace, labels) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*appsv1.StatefulSetList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, labels) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStatefulSets provides a mock function with given fields: ctx, namespace +func (_m *ClientSet) ListStatefulSets(ctx context.Context, namespace string) (*appsv1.StatefulSetList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListStatefulSets") + } + + var r0 *appsv1.StatefulSetList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*appsv1.StatefulSetList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *appsv1.StatefulSetList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*appsv1.StatefulSetList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PatchPodLabel provides a mock function with given fields: ctx, pod, labelkey, labelValue +func (_m *ClientSet) PatchPodLabel(ctx context.Context, pod *corev1.Pod, labelkey string, labelValue string) error { + ret := _m.Called(ctx, pod, labelkey, labelValue) + + if len(ret) == 0 { + panic("no return value specified for PatchPodLabel") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *corev1.Pod, string, string) error); ok { + r0 = rf(ctx, pod, labelkey, labelValue) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RestartDeployment provides a mock function with given fields: ctx, namespace, name +func (_m *ClientSet) RestartDeployment(ctx context.Context, namespace string, name string) (*appsv1.Deployment, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for RestartDeployment") + } + + var r0 *appsv1.Deployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*appsv1.Deployment, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *appsv1.Deployment); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*appsv1.Deployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateClusterRole provides a mock function with given fields: ctx, role +func (_m *ClientSet) UpdateClusterRole(ctx context.Context, role *rbacv1.ClusterRole) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for UpdateClusterRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *rbacv1.ClusterRole) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateClusterRoleBinding provides a mock function with given fields: ctx, role +func (_m *ClientSet) UpdateClusterRoleBinding(ctx context.Context, role *rbacv1.ClusterRoleBinding) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for UpdateClusterRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *rbacv1.ClusterRoleBinding) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateConfigMap provides a mock function with given fields: ctx, namespace, configMap +func (_m *ClientSet) UpdateConfigMap(ctx context.Context, namespace string, configMap *corev1.ConfigMap) error { + ret := _m.Called(ctx, namespace, configMap) + + if len(ret) == 0 { + panic("no return value specified for UpdateConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, configMap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateCronJob provides a mock function with given fields: ctx, namespace, job +func (_m *ClientSet) UpdateCronJob(ctx context.Context, namespace string, job *batchv1.CronJob) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for UpdateCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *batchv1.CronJob) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateDeployment provides a mock function with given fields: ctx, namespace, deployment +func (_m *ClientSet) UpdateDeployment(ctx context.Context, namespace string, deployment *appsv1.Deployment) error { + ret := _m.Called(ctx, namespace, deployment) + + if len(ret) == 0 { + panic("no return value specified for UpdateDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.Deployment) error); ok { + r0 = rf(ctx, namespace, deployment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateDistributedRedisCluster provides a mock function with given fields: ctx, inst +func (_m *ClientSet) UpdateDistributedRedisCluster(ctx context.Context, inst *v1alpha1.DistributedRedisCluster) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateDistributedRedisCluster") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.DistributedRedisCluster) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateDistributedRedisClusterStatus provides a mock function with given fields: ctx, inst +func (_m *ClientSet) UpdateDistributedRedisClusterStatus(ctx context.Context, inst *v1alpha1.DistributedRedisCluster) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateDistributedRedisClusterStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.DistributedRedisCluster) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateIfConfigMapChanged provides a mock function with given fields: ctx, newConfigmap +func (_m *ClientSet) UpdateIfConfigMapChanged(ctx context.Context, newConfigmap *corev1.ConfigMap) error { + ret := _m.Called(ctx, newConfigmap) + + if len(ret) == 0 { + panic("no return value specified for UpdateIfConfigMapChanged") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *corev1.ConfigMap) error); ok { + r0 = rf(ctx, newConfigmap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateIfSelectorChangedService provides a mock function with given fields: ctx, namespace, service +func (_m *ClientSet) UpdateIfSelectorChangedService(ctx context.Context, namespace string, service *corev1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for UpdateIfSelectorChangedService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateJob provides a mock function with given fields: ctx, namespace, job +func (_m *ClientSet) UpdateJob(ctx context.Context, namespace string, job *batchv1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for UpdateJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *batchv1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdatePod provides a mock function with given fields: ctx, namespace, pod +func (_m *ClientSet) UpdatePod(ctx context.Context, namespace string, pod *corev1.Pod) error { + ret := _m.Called(ctx, namespace, pod) + + if len(ret) == 0 { + panic("no return value specified for UpdatePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Pod) error); ok { + r0 = rf(ctx, namespace, pod) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdatePodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *ClientSet) UpdatePodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for UpdatePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *policyv1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRedisFailover provides a mock function with given fields: ctx, inst +func (_m *ClientSet) UpdateRedisFailover(ctx context.Context, inst *databasesv1.RedisFailover) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisFailover") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *databasesv1.RedisFailover) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRedisFailoverStatus provides a mock function with given fields: ctx, inst +func (_m *ClientSet) UpdateRedisFailoverStatus(ctx context.Context, inst *databasesv1.RedisFailover) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisFailoverStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *databasesv1.RedisFailover) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRedisSentinel provides a mock function with given fields: ctx, sen +func (_m *ClientSet) UpdateRedisSentinel(ctx context.Context, sen *databasesv1.RedisSentinel) error { + ret := _m.Called(ctx, sen) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisSentinel") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *databasesv1.RedisSentinel) error); ok { + r0 = rf(ctx, sen) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRedisSentinelStatus provides a mock function with given fields: ctx, inst +func (_m *ClientSet) UpdateRedisSentinelStatus(ctx context.Context, inst *databasesv1.RedisSentinel) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisSentinelStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *databasesv1.RedisSentinel) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRedisUser provides a mock function with given fields: ctx, ru +func (_m *ClientSet) UpdateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *redisv1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRole provides a mock function with given fields: ctx, namespace, role +func (_m *ClientSet) UpdateRole(ctx context.Context, namespace string, role *rbacv1.Role) error { + ret := _m.Called(ctx, namespace, role) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *rbacv1.Role) error); ok { + r0 = rf(ctx, namespace, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *ClientSet) UpdateSecret(ctx context.Context, namespace string, secret *corev1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for UpdateSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateService provides a mock function with given fields: ctx, namespace, service +func (_m *ClientSet) UpdateService(ctx context.Context, namespace string, service *corev1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for UpdateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *corev1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateServiceMonitor provides a mock function with given fields: ctx, namespace, sm +func (_m *ClientSet) UpdateServiceMonitor(ctx context.Context, namespace string, sm *monitoringv1.ServiceMonitor) error { + ret := _m.Called(ctx, namespace, sm) + + if len(ret) == 0 { + panic("no return value specified for UpdateServiceMonitor") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *monitoringv1.ServiceMonitor) error); ok { + r0 = rf(ctx, namespace, sm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateStatefulSet provides a mock function with given fields: ctx, namespace, statefulSet +func (_m *ClientSet) UpdateStatefulSet(ctx context.Context, namespace string, statefulSet *appsv1.StatefulSet) error { + ret := _m.Called(ctx, namespace, statefulSet) + + if len(ret) == 0 { + panic("no return value specified for UpdateStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *appsv1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, statefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewClientSet creates a new instance of ClientSet. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClientSet(t interface { + mock.TestingT + Cleanup(func()) +}) *ClientSet { + mock := &ClientSet{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/config_map.go b/pkg/kubernetes/clientset/mocks/config_map.go new file mode 100644 index 0000000..1c55e0c --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/config_map.go @@ -0,0 +1,213 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" +) + +// ConfigMap is an autogenerated mock type for the ConfigMap type +type ConfigMap struct { + mock.Mock +} + +// CreateConfigMap provides a mock function with given fields: ctx, namespace, configMap +func (_m *ConfigMap) CreateConfigMap(ctx context.Context, namespace string, configMap *v1.ConfigMap) error { + ret := _m.Called(ctx, namespace, configMap) + + if len(ret) == 0 { + panic("no return value specified for CreateConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, configMap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsConfigMap provides a mock function with given fields: ctx, namespace, configMap +func (_m *ConfigMap) CreateIfNotExistsConfigMap(ctx context.Context, namespace string, configMap *v1.ConfigMap) error { + ret := _m.Called(ctx, namespace, configMap) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, configMap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateConfigMap provides a mock function with given fields: ctx, namespace, np +func (_m *ConfigMap) CreateOrUpdateConfigMap(ctx context.Context, namespace string, np *v1.ConfigMap) error { + ret := _m.Called(ctx, namespace, np) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, np) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteConfigMap provides a mock function with given fields: ctx, namespace, name +func (_m *ConfigMap) DeleteConfigMap(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetConfigMap provides a mock function with given fields: ctx, namespace, name +func (_m *ConfigMap) GetConfigMap(ctx context.Context, namespace string, name string) (*v1.ConfigMap, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetConfigMap") + } + + var r0 *v1.ConfigMap + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.ConfigMap, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.ConfigMap); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ConfigMap) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListConfigMaps provides a mock function with given fields: ctx, namespace +func (_m *ConfigMap) ListConfigMaps(ctx context.Context, namespace string) (*v1.ConfigMapList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListConfigMaps") + } + + var r0 *v1.ConfigMapList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.ConfigMapList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.ConfigMapList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ConfigMapList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateConfigMap provides a mock function with given fields: ctx, namespace, configMap +func (_m *ConfigMap) UpdateConfigMap(ctx context.Context, namespace string, configMap *v1.ConfigMap) error { + ret := _m.Called(ctx, namespace, configMap) + + if len(ret) == 0 { + panic("no return value specified for UpdateConfigMap") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ConfigMap) error); ok { + r0 = rf(ctx, namespace, configMap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateIfConfigMapChanged provides a mock function with given fields: ctx, newConfigmap +func (_m *ConfigMap) UpdateIfConfigMapChanged(ctx context.Context, newConfigmap *v1.ConfigMap) error { + ret := _m.Called(ctx, newConfigmap) + + if len(ret) == 0 { + panic("no return value specified for UpdateIfConfigMapChanged") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ConfigMap) error); ok { + r0 = rf(ctx, newConfigmap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewConfigMap creates a new instance of ConfigMap. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewConfigMap(t interface { + mock.TestingT + Cleanup(func()) +}) *ConfigMap { + mock := &ConfigMap{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/cron_job.go b/pkg/kubernetes/clientset/mocks/cron_job.go new file mode 100644 index 0000000..3cf28ce --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/cron_job.go @@ -0,0 +1,180 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + mock "github.com/stretchr/testify/mock" + + v1 "k8s.io/api/batch/v1" +) + +// CronJob is an autogenerated mock type for the CronJob type +type CronJob struct { + mock.Mock +} + +// CreateCronJob provides a mock function with given fields: ctx, namespace, cronjob +func (_m *CronJob) CreateCronJob(ctx context.Context, namespace string, cronjob *v1.CronJob) error { + ret := _m.Called(ctx, namespace, cronjob) + + if len(ret) == 0 { + panic("no return value specified for CreateCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.CronJob) error); ok { + r0 = rf(ctx, namespace, cronjob) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateCronJob provides a mock function with given fields: ctx, namespace, cronjob +func (_m *CronJob) CreateOrUpdateCronJob(ctx context.Context, namespace string, cronjob *v1.CronJob) error { + ret := _m.Called(ctx, namespace, cronjob) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.CronJob) error); ok { + r0 = rf(ctx, namespace, cronjob) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteCronJob provides a mock function with given fields: ctx, namespace, name +func (_m *CronJob) DeleteCronJob(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetCronJob provides a mock function with given fields: ctx, namespace, name +func (_m *CronJob) GetCronJob(ctx context.Context, namespace string, name string) (*v1.CronJob, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetCronJob") + } + + var r0 *v1.CronJob + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.CronJob, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.CronJob); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.CronJob) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListCronJobs provides a mock function with given fields: ctx, namespace, cl +func (_m *CronJob) ListCronJobs(ctx context.Context, namespace string, cl client.ListOptions) (*v1.CronJobList, error) { + ret := _m.Called(ctx, namespace, cl) + + if len(ret) == 0 { + panic("no return value specified for ListCronJobs") + } + + var r0 *v1.CronJobList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*v1.CronJobList, error)); ok { + return rf(ctx, namespace, cl) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *v1.CronJobList); ok { + r0 = rf(ctx, namespace, cl) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.CronJobList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, cl) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateCronJob provides a mock function with given fields: ctx, namespace, job +func (_m *CronJob) UpdateCronJob(ctx context.Context, namespace string, job *v1.CronJob) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for UpdateCronJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.CronJob) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewCronJob creates a new instance of CronJob. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCronJob(t interface { + mock.TestingT + Cleanup(func()) +}) *CronJob { + mock := &CronJob{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/deployment.go b/pkg/kubernetes/clientset/mocks/deployment.go new file mode 100644 index 0000000..5acc943 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/deployment.go @@ -0,0 +1,258 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + corev1 "k8s.io/api/core/v1" + + mock "github.com/stretchr/testify/mock" + + v1 "k8s.io/api/apps/v1" +) + +// Deployment is an autogenerated mock type for the Deployment type +type Deployment struct { + mock.Mock +} + +// CreateDeployment provides a mock function with given fields: ctx, namespace, deployment +func (_m *Deployment) CreateDeployment(ctx context.Context, namespace string, deployment *v1.Deployment) error { + ret := _m.Called(ctx, namespace, deployment) + + if len(ret) == 0 { + panic("no return value specified for CreateDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Deployment) error); ok { + r0 = rf(ctx, namespace, deployment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateIfNotExistsDeployment provides a mock function with given fields: ctx, namespace, deploy +func (_m *Deployment) CreateIfNotExistsDeployment(ctx context.Context, namespace string, deploy *v1.Deployment) error { + ret := _m.Called(ctx, namespace, deploy) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Deployment) error); ok { + r0 = rf(ctx, namespace, deploy) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateDeployment provides a mock function with given fields: ctx, namespace, deployment +func (_m *Deployment) CreateOrUpdateDeployment(ctx context.Context, namespace string, deployment *v1.Deployment) error { + ret := _m.Called(ctx, namespace, deployment) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Deployment) error); ok { + r0 = rf(ctx, namespace, deployment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteDeployment provides a mock function with given fields: ctx, namespace, name +func (_m *Deployment) DeleteDeployment(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetDeployment provides a mock function with given fields: ctx, namespace, name +func (_m *Deployment) GetDeployment(ctx context.Context, namespace string, name string) (*v1.Deployment, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetDeployment") + } + + var r0 *v1.Deployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Deployment, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Deployment); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Deployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetDeploymentPods provides a mock function with given fields: ctx, namespace, name +func (_m *Deployment) GetDeploymentPods(ctx context.Context, namespace string, name string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetDeploymentPods") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.PodList); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListDeployments provides a mock function with given fields: ctx, namespace +func (_m *Deployment) ListDeployments(ctx context.Context, namespace string) (*v1.DeploymentList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListDeployments") + } + + var r0 *v1.DeploymentList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.DeploymentList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.DeploymentList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.DeploymentList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RestartDeployment provides a mock function with given fields: ctx, namespace, name +func (_m *Deployment) RestartDeployment(ctx context.Context, namespace string, name string) (*v1.Deployment, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for RestartDeployment") + } + + var r0 *v1.Deployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Deployment, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Deployment); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Deployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateDeployment provides a mock function with given fields: ctx, namespace, deployment +func (_m *Deployment) UpdateDeployment(ctx context.Context, namespace string, deployment *v1.Deployment) error { + ret := _m.Called(ctx, namespace, deployment) + + if len(ret) == 0 { + panic("no return value specified for UpdateDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Deployment) error); ok { + r0 = rf(ctx, namespace, deployment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewDeployment creates a new instance of Deployment. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDeployment(t interface { + mock.TestingT + Cleanup(func()) +}) *Deployment { + mock := &Deployment{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/distributed_redis_cluster.go b/pkg/kubernetes/clientset/mocks/distributed_redis_cluster.go new file mode 100644 index 0000000..554af6f --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/distributed_redis_cluster.go @@ -0,0 +1,111 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + v1alpha1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + mock "github.com/stretchr/testify/mock" +) + +// DistributedRedisCluster is an autogenerated mock type for the DistributedRedisCluster type +type DistributedRedisCluster struct { + mock.Mock +} + +// GetDistributedRedisCluster provides a mock function with given fields: ctx, namespace, name +func (_m *DistributedRedisCluster) GetDistributedRedisCluster(ctx context.Context, namespace string, name string) (*v1alpha1.DistributedRedisCluster, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetDistributedRedisCluster") + } + + var r0 *v1alpha1.DistributedRedisCluster + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1alpha1.DistributedRedisCluster, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1alpha1.DistributedRedisCluster); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1alpha1.DistributedRedisCluster) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateDistributedRedisCluster provides a mock function with given fields: ctx, inst +func (_m *DistributedRedisCluster) UpdateDistributedRedisCluster(ctx context.Context, inst *v1alpha1.DistributedRedisCluster) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateDistributedRedisCluster") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.DistributedRedisCluster) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateDistributedRedisClusterStatus provides a mock function with given fields: ctx, inst +func (_m *DistributedRedisCluster) UpdateDistributedRedisClusterStatus(ctx context.Context, inst *v1alpha1.DistributedRedisCluster) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateDistributedRedisClusterStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.DistributedRedisCluster) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewDistributedRedisCluster creates a new instance of DistributedRedisCluster. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDistributedRedisCluster(t interface { + mock.TestingT + Cleanup(func()) +}) *DistributedRedisCluster { + mock := &DistributedRedisCluster{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/job.go b/pkg/kubernetes/clientset/mocks/job.go new file mode 100644 index 0000000..311056c --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/job.go @@ -0,0 +1,228 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + mock "github.com/stretchr/testify/mock" + + v1 "k8s.io/api/batch/v1" +) + +// Job is an autogenerated mock type for the Job type +type Job struct { + mock.Mock +} + +// CreateIfNotExistsJob provides a mock function with given fields: ctx, namespace, job +func (_m *Job) CreateIfNotExistsJob(ctx context.Context, namespace string, job *v1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateJob provides a mock function with given fields: ctx, namespace, job +func (_m *Job) CreateJob(ctx context.Context, namespace string, job *v1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for CreateJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateJob provides a mock function with given fields: ctx, namespace, job +func (_m *Job) CreateOrUpdateJob(ctx context.Context, namespace string, job *v1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteJob provides a mock function with given fields: ctx, namespace, name +func (_m *Job) DeleteJob(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetJob provides a mock function with given fields: ctx, namespace, name +func (_m *Job) GetJob(ctx context.Context, namespace string, name string) (*v1.Job, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetJob") + } + + var r0 *v1.Job + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Job, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Job); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Job) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListJobs provides a mock function with given fields: ctx, namespace, cl +func (_m *Job) ListJobs(ctx context.Context, namespace string, cl client.ListOptions) (*v1.JobList, error) { + ret := _m.Called(ctx, namespace, cl) + + if len(ret) == 0 { + panic("no return value specified for ListJobs") + } + + var r0 *v1.JobList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*v1.JobList, error)); ok { + return rf(ctx, namespace, cl) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *v1.JobList); ok { + r0 = rf(ctx, namespace, cl) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.JobList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, cl) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListJobsByLabel provides a mock function with given fields: ctx, namespace, label_map +func (_m *Job) ListJobsByLabel(ctx context.Context, namespace string, label_map map[string]string) (*v1.JobList, error) { + ret := _m.Called(ctx, namespace, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListJobsByLabel") + } + + var r0 *v1.JobList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*v1.JobList, error)); ok { + return rf(ctx, namespace, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *v1.JobList); ok { + r0 = rf(ctx, namespace, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.JobList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateJob provides a mock function with given fields: ctx, namespace, job +func (_m *Job) UpdateJob(ctx context.Context, namespace string, job *v1.Job) error { + ret := _m.Called(ctx, namespace, job) + + if len(ret) == 0 { + panic("no return value specified for UpdateJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Job) error); ok { + r0 = rf(ctx, namespace, job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewJob creates a new instance of Job. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewJob(t interface { + mock.TestingT + Cleanup(func()) +}) *Job { + mock := &Job{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/name_spaces.go b/pkg/kubernetes/clientset/mocks/name_spaces.go new file mode 100644 index 0000000..83bccae --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/name_spaces.go @@ -0,0 +1,75 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" +) + +// NameSpaces is an autogenerated mock type for the NameSpaces type +type NameSpaces struct { + mock.Mock +} + +// GetNameSpace provides a mock function with given fields: ctx, namespace +func (_m *NameSpaces) GetNameSpace(ctx context.Context, namespace string) (*v1.Namespace, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for GetNameSpace") + } + + var r0 *v1.Namespace + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.Namespace, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.Namespace); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Namespace) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewNameSpaces creates a new instance of NameSpaces. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewNameSpaces(t interface { + mock.TestingT + Cleanup(func()) +}) *NameSpaces { + mock := &NameSpaces{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/node.go b/pkg/kubernetes/clientset/mocks/node.go new file mode 100644 index 0000000..21f5e9d --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/node.go @@ -0,0 +1,105 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" +) + +// Node is an autogenerated mock type for the Node type +type Node struct { + mock.Mock +} + +// GetNode provides a mock function with given fields: ctx, name +func (_m *Node) GetNode(ctx context.Context, name string) (*v1.Node, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetNode") + } + + var r0 *v1.Node + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.Node, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.Node); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Node) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListNodesByLabels provides a mock function with given fields: ctx, label_map +func (_m *Node) ListNodesByLabels(ctx context.Context, label_map map[string]string) (*v1.NodeList, error) { + ret := _m.Called(ctx, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListNodesByLabels") + } + + var r0 *v1.NodeList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, map[string]string) (*v1.NodeList, error)); ok { + return rf(ctx, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, map[string]string) *v1.NodeList); ok { + r0 = rf(ctx, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.NodeList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, map[string]string) error); ok { + r1 = rf(ctx, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewNode creates a new instance of Node. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewNode(t interface { + mock.TestingT + Cleanup(func()) +}) *Node { + mock := &Node{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/pod.go b/pkg/kubernetes/clientset/mocks/pod.go new file mode 100644 index 0000000..40063d2 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/pod.go @@ -0,0 +1,276 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + io "io" + + mock "github.com/stretchr/testify/mock" + + v1 "k8s.io/api/core/v1" +) + +// Pod is an autogenerated mock type for the Pod type +type Pod struct { + mock.Mock +} + +// CreateOrUpdatePod provides a mock function with given fields: ctx, namespace, pod +func (_m *Pod) CreateOrUpdatePod(ctx context.Context, namespace string, pod *v1.Pod) error { + ret := _m.Called(ctx, namespace, pod) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdatePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Pod) error); ok { + r0 = rf(ctx, namespace, pod) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreatePod provides a mock function with given fields: ctx, namespace, pod +func (_m *Pod) CreatePod(ctx context.Context, namespace string, pod *v1.Pod) error { + ret := _m.Called(ctx, namespace, pod) + + if len(ret) == 0 { + panic("no return value specified for CreatePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Pod) error); ok { + r0 = rf(ctx, namespace, pod) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeletePod provides a mock function with given fields: ctx, namespace, name, opts +func (_m *Pod) DeletePod(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, namespace, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeletePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...client.DeleteOption) error); ok { + r0 = rf(ctx, namespace, name, opts...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Exec provides a mock function with given fields: ctx, namespace, name, containerName, cmd +func (_m *Pod) Exec(ctx context.Context, namespace string, name string, containerName string, cmd []string) (io.Reader, io.Reader, error) { + ret := _m.Called(ctx, namespace, name, containerName, cmd) + + if len(ret) == 0 { + panic("no return value specified for Exec") + } + + var r0 io.Reader + var r1 io.Reader + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, []string) (io.Reader, io.Reader, error)); ok { + return rf(ctx, namespace, name, containerName, cmd) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, []string) io.Reader); ok { + r0 = rf(ctx, namespace, name, containerName, cmd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.Reader) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, []string) io.Reader); ok { + r1 = rf(ctx, namespace, name, containerName, cmd) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(io.Reader) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, string, []string) error); ok { + r2 = rf(ctx, namespace, name, containerName, cmd) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetPod provides a mock function with given fields: ctx, namespace, name +func (_m *Pod) GetPod(ctx context.Context, namespace string, name string) (*v1.Pod, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetPod") + } + + var r0 *v1.Pod + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Pod, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Pod); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Pod) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListPodByLabels provides a mock function with given fields: ctx, namespace, label_map +func (_m *Pod) ListPodByLabels(ctx context.Context, namespace string, label_map map[string]string) (*v1.PodList, error) { + ret := _m.Called(ctx, namespace, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListPodByLabels") + } + + var r0 *v1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*v1.PodList, error)); ok { + return rf(ctx, namespace, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *v1.PodList); ok { + r0 = rf(ctx, namespace, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListPods provides a mock function with given fields: ctx, namespace +func (_m *Pod) ListPods(ctx context.Context, namespace string) (*v1.PodList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListPods") + } + + var r0 *v1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.PodList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.PodList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PatchPodLabel provides a mock function with given fields: ctx, pod, labelkey, labelValue +func (_m *Pod) PatchPodLabel(ctx context.Context, pod *v1.Pod, labelkey string, labelValue string) error { + ret := _m.Called(ctx, pod, labelkey, labelValue) + + if len(ret) == 0 { + panic("no return value specified for PatchPodLabel") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.Pod, string, string) error); ok { + r0 = rf(ctx, pod, labelkey, labelValue) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdatePod provides a mock function with given fields: ctx, namespace, pod +func (_m *Pod) UpdatePod(ctx context.Context, namespace string, pod *v1.Pod) error { + ret := _m.Called(ctx, namespace, pod) + + if len(ret) == 0 { + panic("no return value specified for UpdatePod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Pod) error); ok { + r0 = rf(ctx, namespace, pod) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewPod creates a new instance of Pod. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPod(t interface { + mock.TestingT + Cleanup(func()) +}) *Pod { + mock := &Pod{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/pod_disruption_budget.go b/pkg/kubernetes/clientset/mocks/pod_disruption_budget.go new file mode 100644 index 0000000..dc2b257 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/pod_disruption_budget.go @@ -0,0 +1,165 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/policy/v1" +) + +// PodDisruptionBudget is an autogenerated mock type for the PodDisruptionBudget type +type PodDisruptionBudget struct { + mock.Mock +} + +// CreateIfNotExistsPodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *PodDisruptionBudget) CreateIfNotExistsPodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *v1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsPodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdatePodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *PodDisruptionBudget) CreateOrUpdatePodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *v1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdatePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreatePodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *PodDisruptionBudget) CreatePodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *v1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for CreatePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeletePodDisruptionBudget provides a mock function with given fields: ctx, namespace, name +func (_m *PodDisruptionBudget) DeletePodDisruptionBudget(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeletePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetPodDisruptionBudget provides a mock function with given fields: ctx, namespace, name +func (_m *PodDisruptionBudget) GetPodDisruptionBudget(ctx context.Context, namespace string, name string) (*v1.PodDisruptionBudget, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetPodDisruptionBudget") + } + + var r0 *v1.PodDisruptionBudget + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.PodDisruptionBudget, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.PodDisruptionBudget); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.PodDisruptionBudget) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdatePodDisruptionBudget provides a mock function with given fields: ctx, namespace, podDisruptionBudget +func (_m *PodDisruptionBudget) UpdatePodDisruptionBudget(ctx context.Context, namespace string, podDisruptionBudget *v1.PodDisruptionBudget) error { + ret := _m.Called(ctx, namespace, podDisruptionBudget) + + if len(ret) == 0 { + panic("no return value specified for UpdatePodDisruptionBudget") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.PodDisruptionBudget) error); ok { + r0 = rf(ctx, namespace, podDisruptionBudget) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewPodDisruptionBudget creates a new instance of PodDisruptionBudget. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPodDisruptionBudget(t interface { + mock.TestingT + Cleanup(func()) +}) *PodDisruptionBudget { + mock := &PodDisruptionBudget{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/pvc.go b/pkg/kubernetes/clientset/mocks/pvc.go new file mode 100644 index 0000000..eeec0f1 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/pvc.go @@ -0,0 +1,123 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" +) + +// PVC is an autogenerated mock type for the PVC type +type PVC struct { + mock.Mock +} + +// CreatePVC provides a mock function with given fields: ctx, namespace, pvc +func (_m *PVC) CreatePVC(ctx context.Context, namespace string, pvc *v1.PersistentVolumeClaim) error { + ret := _m.Called(ctx, namespace, pvc) + + if len(ret) == 0 { + panic("no return value specified for CreatePVC") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.PersistentVolumeClaim) error); ok { + r0 = rf(ctx, namespace, pvc) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetPVC provides a mock function with given fields: ctx, namespace, name +func (_m *PVC) GetPVC(ctx context.Context, namespace string, name string) (*v1.PersistentVolumeClaim, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetPVC") + } + + var r0 *v1.PersistentVolumeClaim + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.PersistentVolumeClaim, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.PersistentVolumeClaim); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.PersistentVolumeClaim) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListPvcByLabel provides a mock function with given fields: ctx, namespace, label_map +func (_m *PVC) ListPvcByLabel(ctx context.Context, namespace string, label_map map[string]string) (*v1.PersistentVolumeClaimList, error) { + ret := _m.Called(ctx, namespace, label_map) + + if len(ret) == 0 { + panic("no return value specified for ListPvcByLabel") + } + + var r0 *v1.PersistentVolumeClaimList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*v1.PersistentVolumeClaimList, error)); ok { + return rf(ctx, namespace, label_map) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *v1.PersistentVolumeClaimList); ok { + r0 = rf(ctx, namespace, label_map) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.PersistentVolumeClaimList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, label_map) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewPVC creates a new instance of PVC. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPVC(t interface { + mock.TestingT + Cleanup(func()) +}) *PVC { + mock := &PVC{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/rbac.go b/pkg/kubernetes/clientset/mocks/rbac.go new file mode 100644 index 0000000..6a4e375 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/rbac.go @@ -0,0 +1,363 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/rbac/v1" +) + +// RBAC is an autogenerated mock type for the RBAC type +type RBAC struct { + mock.Mock +} + +// CreateClusterRole provides a mock function with given fields: ctx, role +func (_m *RBAC) CreateClusterRole(ctx context.Context, role *v1.ClusterRole) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for CreateClusterRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ClusterRole) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateClusterRoleBinding provides a mock function with given fields: ctx, rb +func (_m *RBAC) CreateClusterRoleBinding(ctx context.Context, rb *v1.ClusterRoleBinding) error { + ret := _m.Called(ctx, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateClusterRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ClusterRoleBinding) error); ok { + r0 = rf(ctx, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateClusterRole provides a mock function with given fields: ctx, role +func (_m *RBAC) CreateOrUpdateClusterRole(ctx context.Context, role *v1.ClusterRole) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateClusterRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ClusterRole) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateClusterRoleBinding provides a mock function with given fields: ctx, rb +func (_m *RBAC) CreateOrUpdateClusterRoleBinding(ctx context.Context, rb *v1.ClusterRoleBinding) error { + ret := _m.Called(ctx, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateClusterRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ClusterRoleBinding) error); ok { + r0 = rf(ctx, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateRole provides a mock function with given fields: ctx, namespace, role +func (_m *RBAC) CreateOrUpdateRole(ctx context.Context, namespace string, role *v1.Role) error { + ret := _m.Called(ctx, namespace, role) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Role) error); ok { + r0 = rf(ctx, namespace, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateRoleBinding provides a mock function with given fields: ctx, namespace, rb +func (_m *RBAC) CreateOrUpdateRoleBinding(ctx context.Context, namespace string, rb *v1.RoleBinding) error { + ret := _m.Called(ctx, namespace, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.RoleBinding) error); ok { + r0 = rf(ctx, namespace, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateRole provides a mock function with given fields: ctx, namespace, role +func (_m *RBAC) CreateRole(ctx context.Context, namespace string, role *v1.Role) error { + ret := _m.Called(ctx, namespace, role) + + if len(ret) == 0 { + panic("no return value specified for CreateRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Role) error); ok { + r0 = rf(ctx, namespace, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateRoleBinding provides a mock function with given fields: ctx, namespace, rb +func (_m *RBAC) CreateRoleBinding(ctx context.Context, namespace string, rb *v1.RoleBinding) error { + ret := _m.Called(ctx, namespace, rb) + + if len(ret) == 0 { + panic("no return value specified for CreateRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.RoleBinding) error); ok { + r0 = rf(ctx, namespace, rb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetClusterRole provides a mock function with given fields: ctx, name +func (_m *RBAC) GetClusterRole(ctx context.Context, name string) (*v1.ClusterRole, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetClusterRole") + } + + var r0 *v1.ClusterRole + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.ClusterRole, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.ClusterRole); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ClusterRole) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetClusterRoleBinding provides a mock function with given fields: ctx, name +func (_m *RBAC) GetClusterRoleBinding(ctx context.Context, name string) (*v1.ClusterRoleBinding, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetClusterRoleBinding") + } + + var r0 *v1.ClusterRoleBinding + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.ClusterRoleBinding, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.ClusterRoleBinding); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ClusterRoleBinding) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRole provides a mock function with given fields: ctx, namespace, name +func (_m *RBAC) GetRole(ctx context.Context, namespace string, name string) (*v1.Role, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRole") + } + + var r0 *v1.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Role, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Role); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRoleBinding provides a mock function with given fields: ctx, namespace, name +func (_m *RBAC) GetRoleBinding(ctx context.Context, namespace string, name string) (*v1.RoleBinding, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRoleBinding") + } + + var r0 *v1.RoleBinding + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.RoleBinding, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.RoleBinding); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RoleBinding) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateClusterRole provides a mock function with given fields: ctx, role +func (_m *RBAC) UpdateClusterRole(ctx context.Context, role *v1.ClusterRole) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for UpdateClusterRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ClusterRole) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateClusterRoleBinding provides a mock function with given fields: ctx, role +func (_m *RBAC) UpdateClusterRoleBinding(ctx context.Context, role *v1.ClusterRoleBinding) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for UpdateClusterRoleBinding") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ClusterRoleBinding) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRole provides a mock function with given fields: ctx, namespace, role +func (_m *RBAC) UpdateRole(ctx context.Context, namespace string, role *v1.Role) error { + ret := _m.Called(ctx, namespace, role) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Role) error); ok { + r0 = rf(ctx, namespace, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewRBAC creates a new instance of RBAC. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRBAC(t interface { + mock.TestingT + Cleanup(func()) +}) *RBAC { + mock := &RBAC{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/redis_failover.go b/pkg/kubernetes/clientset/mocks/redis_failover.go new file mode 100644 index 0000000..83051b0 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/redis_failover.go @@ -0,0 +1,144 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + mock "github.com/stretchr/testify/mock" + + v1 "github.com/alauda/redis-operator/api/databases/v1" +) + +// RedisFailover is an autogenerated mock type for the RedisFailover type +type RedisFailover struct { + mock.Mock +} + +// GetRedisFailover provides a mock function with given fields: ctx, namespace, name +func (_m *RedisFailover) GetRedisFailover(ctx context.Context, namespace string, name string) (*v1.RedisFailover, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRedisFailover") + } + + var r0 *v1.RedisFailover + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.RedisFailover, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.RedisFailover); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RedisFailover) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRedisFailovers provides a mock function with given fields: ctx, namespace, opts +func (_m *RedisFailover) ListRedisFailovers(ctx context.Context, namespace string, opts client.ListOptions) (*v1.RedisFailoverList, error) { + ret := _m.Called(ctx, namespace, opts) + + if len(ret) == 0 { + panic("no return value specified for ListRedisFailovers") + } + + var r0 *v1.RedisFailoverList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*v1.RedisFailoverList, error)); ok { + return rf(ctx, namespace, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *v1.RedisFailoverList); ok { + r0 = rf(ctx, namespace, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RedisFailoverList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRedisFailover provides a mock function with given fields: ctx, inst +func (_m *RedisFailover) UpdateRedisFailover(ctx context.Context, inst *v1.RedisFailover) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisFailover") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisFailover) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRedisFailoverStatus provides a mock function with given fields: ctx, inst +func (_m *RedisFailover) UpdateRedisFailoverStatus(ctx context.Context, inst *v1.RedisFailover) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisFailoverStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisFailover) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewRedisFailover creates a new instance of RedisFailover. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRedisFailover(t interface { + mock.TestingT + Cleanup(func()) +}) *RedisFailover { + mock := &RedisFailover{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/redis_sentinel.go b/pkg/kubernetes/clientset/mocks/redis_sentinel.go new file mode 100644 index 0000000..20d4415 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/redis_sentinel.go @@ -0,0 +1,144 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + mock "github.com/stretchr/testify/mock" + + v1 "github.com/alauda/redis-operator/api/databases/v1" +) + +// RedisSentinel is an autogenerated mock type for the RedisSentinel type +type RedisSentinel struct { + mock.Mock +} + +// GetRedisSentinel provides a mock function with given fields: ctx, namespace, name +func (_m *RedisSentinel) GetRedisSentinel(ctx context.Context, namespace string, name string) (*v1.RedisSentinel, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRedisSentinel") + } + + var r0 *v1.RedisSentinel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.RedisSentinel, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.RedisSentinel); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RedisSentinel) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRedisSentinels provides a mock function with given fields: ctx, namespace, opts +func (_m *RedisSentinel) ListRedisSentinels(ctx context.Context, namespace string, opts client.ListOptions) (*v1.RedisSentinelList, error) { + ret := _m.Called(ctx, namespace, opts) + + if len(ret) == 0 { + panic("no return value specified for ListRedisSentinels") + } + + var r0 *v1.RedisSentinelList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*v1.RedisSentinelList, error)); ok { + return rf(ctx, namespace, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *v1.RedisSentinelList); ok { + r0 = rf(ctx, namespace, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RedisSentinelList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRedisSentinel provides a mock function with given fields: ctx, sen +func (_m *RedisSentinel) UpdateRedisSentinel(ctx context.Context, sen *v1.RedisSentinel) error { + ret := _m.Called(ctx, sen) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisSentinel") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisSentinel) error); ok { + r0 = rf(ctx, sen) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRedisSentinelStatus provides a mock function with given fields: ctx, inst +func (_m *RedisSentinel) UpdateRedisSentinelStatus(ctx context.Context, inst *v1.RedisSentinel) error { + ret := _m.Called(ctx, inst) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisSentinelStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisSentinel) error); ok { + r0 = rf(ctx, inst) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewRedisSentinel creates a new instance of RedisSentinel. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRedisSentinel(t interface { + mock.TestingT + Cleanup(func()) +}) *RedisSentinel { + mock := &RedisSentinel{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/redis_user.go b/pkg/kubernetes/clientset/mocks/redis_user.go new file mode 100644 index 0000000..bf5e01c --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/redis_user.go @@ -0,0 +1,180 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + mock "github.com/stretchr/testify/mock" + + v1 "github.com/alauda/redis-operator/api/middleware/redis/v1" +) + +// RedisUser is an autogenerated mock type for the RedisUser type +type RedisUser struct { + mock.Mock +} + +// CreateIfNotExistsRedisUser provides a mock function with given fields: ctx, ru +func (_m *RedisUser) CreateIfNotExistsRedisUser(ctx context.Context, ru *v1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateRedisUser provides a mock function with given fields: ctx, ru +func (_m *RedisUser) CreateOrUpdateRedisUser(ctx context.Context, ru *v1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateRedisUser provides a mock function with given fields: ctx, ru +func (_m *RedisUser) CreateRedisUser(ctx context.Context, ru *v1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for CreateRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetRedisUser provides a mock function with given fields: ctx, namespace, name +func (_m *RedisUser) GetRedisUser(ctx context.Context, namespace string, name string) (*v1.RedisUser, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetRedisUser") + } + + var r0 *v1.RedisUser + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.RedisUser, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.RedisUser); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RedisUser) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRedisUsers provides a mock function with given fields: ctx, namespace, opts +func (_m *RedisUser) ListRedisUsers(ctx context.Context, namespace string, opts client.ListOptions) (*v1.RedisUserList, error) { + ret := _m.Called(ctx, namespace, opts) + + if len(ret) == 0 { + panic("no return value specified for ListRedisUsers") + } + + var r0 *v1.RedisUserList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) (*v1.RedisUserList, error)); ok { + return rf(ctx, namespace, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.ListOptions) *v1.RedisUserList); ok { + r0 = rf(ctx, namespace, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RedisUserList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.ListOptions) error); ok { + r1 = rf(ctx, namespace, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRedisUser provides a mock function with given fields: ctx, ru +func (_m *RedisUser) UpdateRedisUser(ctx context.Context, ru *v1.RedisUser) error { + ret := _m.Called(ctx, ru) + + if len(ret) == 0 { + panic("no return value specified for UpdateRedisUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RedisUser) error); ok { + r0 = rf(ctx, ru) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewRedisUser creates a new instance of RedisUser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRedisUser(t interface { + mock.TestingT + Cleanup(func()) +}) *RedisUser { + mock := &RedisUser{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/secret.go b/pkg/kubernetes/clientset/mocks/secret.go new file mode 100644 index 0000000..213a9bc --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/secret.go @@ -0,0 +1,195 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" +) + +// Secret is an autogenerated mock type for the Secret type +type Secret struct { + mock.Mock +} + +// CreateIfNotExistsSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *Secret) CreateIfNotExistsSecret(ctx context.Context, namespace string, secret *v1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *Secret) CreateOrUpdateSecret(ctx context.Context, namespace string, secret *v1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *Secret) CreateSecret(ctx context.Context, namespace string, secret *v1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for CreateSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteSecret provides a mock function with given fields: ctx, namespace, name +func (_m *Secret) DeleteSecret(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetSecret provides a mock function with given fields: ctx, namespace, name +func (_m *Secret) GetSecret(ctx context.Context, namespace string, name string) (*v1.Secret, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetSecret") + } + + var r0 *v1.Secret + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Secret, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Secret); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Secret) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListSecret provides a mock function with given fields: ctx, namespace +func (_m *Secret) ListSecret(ctx context.Context, namespace string) (*v1.SecretList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListSecret") + } + + var r0 *v1.SecretList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.SecretList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.SecretList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.SecretList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateSecret provides a mock function with given fields: ctx, namespace, secret +func (_m *Secret) UpdateSecret(ctx context.Context, namespace string, secret *v1.Secret) error { + ret := _m.Called(ctx, namespace, secret) + + if len(ret) == 0 { + panic("no return value specified for UpdateSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Secret) error); ok { + r0 = rf(ctx, namespace, secret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewSecret creates a new instance of Secret. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSecret(t interface { + mock.TestingT + Cleanup(func()) +}) *Secret { + mock := &Secret{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/service.go b/pkg/kubernetes/clientset/mocks/service.go new file mode 100644 index 0000000..4f22939 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/service.go @@ -0,0 +1,261 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" +) + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +// CreateIfNotExistsService provides a mock function with given fields: ctx, namespace, service +func (_m *Service) CreateIfNotExistsService(ctx context.Context, namespace string, service *v1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateIfServiceChanged provides a mock function with given fields: ctx, namespace, service +func (_m *Service) CreateOrUpdateIfServiceChanged(ctx context.Context, namespace string, service *v1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateIfServiceChanged") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateService provides a mock function with given fields: ctx, namespace, service +func (_m *Service) CreateOrUpdateService(ctx context.Context, namespace string, service *v1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateService provides a mock function with given fields: ctx, namespace, service +func (_m *Service) CreateService(ctx context.Context, namespace string, service *v1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for CreateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteService provides a mock function with given fields: ctx, namespace, name +func (_m *Service) DeleteService(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetService provides a mock function with given fields: ctx, namespace, name +func (_m *Service) GetService(ctx context.Context, namespace string, name string) (*v1.Service, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetService") + } + + var r0 *v1.Service + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Service, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Service); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Service) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetServiceByLabels provides a mock function with given fields: ctx, namespace, labelsMap +func (_m *Service) GetServiceByLabels(ctx context.Context, namespace string, labelsMap map[string]string) (*v1.ServiceList, error) { + ret := _m.Called(ctx, namespace, labelsMap) + + if len(ret) == 0 { + panic("no return value specified for GetServiceByLabels") + } + + var r0 *v1.ServiceList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*v1.ServiceList, error)); ok { + return rf(ctx, namespace, labelsMap) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *v1.ServiceList); ok { + r0 = rf(ctx, namespace, labelsMap) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ServiceList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, labelsMap) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListServices provides a mock function with given fields: ctx, namespace +func (_m *Service) ListServices(ctx context.Context, namespace string) (*v1.ServiceList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListServices") + } + + var r0 *v1.ServiceList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.ServiceList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.ServiceList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ServiceList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateIfSelectorChangedService provides a mock function with given fields: ctx, namespace, service +func (_m *Service) UpdateIfSelectorChangedService(ctx context.Context, namespace string, service *v1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for UpdateIfSelectorChangedService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateService provides a mock function with given fields: ctx, namespace, service +func (_m *Service) UpdateService(ctx context.Context, namespace string, service *v1.Service) error { + ret := _m.Called(ctx, namespace, service) + + if len(ret) == 0 { + panic("no return value specified for UpdateService") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.Service) error); ok { + r0 = rf(ctx, namespace, service) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/service_account.go b/pkg/kubernetes/clientset/mocks/service_account.go new file mode 100644 index 0000000..981a516 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/service_account.go @@ -0,0 +1,111 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" +) + +// ServiceAccount is an autogenerated mock type for the ServiceAccount type +type ServiceAccount struct { + mock.Mock +} + +// CreateOrUpdateServiceAccount provides a mock function with given fields: ctx, namespace, sa +func (_m *ServiceAccount) CreateOrUpdateServiceAccount(ctx context.Context, namespace string, sa *v1.ServiceAccount) error { + ret := _m.Called(ctx, namespace, sa) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateServiceAccount") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ServiceAccount) error); ok { + r0 = rf(ctx, namespace, sa) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateServiceAccount provides a mock function with given fields: ctx, namespace, sa +func (_m *ServiceAccount) CreateServiceAccount(ctx context.Context, namespace string, sa *v1.ServiceAccount) error { + ret := _m.Called(ctx, namespace, sa) + + if len(ret) == 0 { + panic("no return value specified for CreateServiceAccount") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ServiceAccount) error); ok { + r0 = rf(ctx, namespace, sa) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetServiceAccount provides a mock function with given fields: ctx, namespace, name +func (_m *ServiceAccount) GetServiceAccount(ctx context.Context, namespace string, name string) (*v1.ServiceAccount, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetServiceAccount") + } + + var r0 *v1.ServiceAccount + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.ServiceAccount, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.ServiceAccount); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ServiceAccount) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewServiceAccount creates a new instance of ServiceAccount. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewServiceAccount(t interface { + mock.TestingT + Cleanup(func()) +}) *ServiceAccount { + mock := &ServiceAccount{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/service_monitor.go b/pkg/kubernetes/clientset/mocks/service_monitor.go new file mode 100644 index 0000000..928c921 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/service_monitor.go @@ -0,0 +1,129 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + mock "github.com/stretchr/testify/mock" +) + +// ServiceMonitor is an autogenerated mock type for the ServiceMonitor type +type ServiceMonitor struct { + mock.Mock +} + +// CreateOrUpdateServiceMonitor provides a mock function with given fields: ctx, namespace, sm +func (_m *ServiceMonitor) CreateOrUpdateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error { + ret := _m.Called(ctx, namespace, sm) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateServiceMonitor") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ServiceMonitor) error); ok { + r0 = rf(ctx, namespace, sm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateServiceMonitor provides a mock function with given fields: ctx, namespace, sm +func (_m *ServiceMonitor) CreateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error { + ret := _m.Called(ctx, namespace, sm) + + if len(ret) == 0 { + panic("no return value specified for CreateServiceMonitor") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ServiceMonitor) error); ok { + r0 = rf(ctx, namespace, sm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetServiceMonitor provides a mock function with given fields: ctx, namespace, name +func (_m *ServiceMonitor) GetServiceMonitor(ctx context.Context, namespace string, name string) (*v1.ServiceMonitor, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetServiceMonitor") + } + + var r0 *v1.ServiceMonitor + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.ServiceMonitor, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.ServiceMonitor); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ServiceMonitor) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateServiceMonitor provides a mock function with given fields: ctx, namespace, sm +func (_m *ServiceMonitor) UpdateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error { + ret := _m.Called(ctx, namespace, sm) + + if len(ret) == 0 { + panic("no return value specified for UpdateServiceMonitor") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.ServiceMonitor) error); ok { + r0 = rf(ctx, namespace, sm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewServiceMonitor creates a new instance of ServiceMonitor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewServiceMonitor(t interface { + mock.TestingT + Cleanup(func()) +}) *ServiceMonitor { + mock := &ServiceMonitor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/mocks/stateful_set.go b/pkg/kubernetes/clientset/mocks/stateful_set.go new file mode 100644 index 0000000..419e673 --- /dev/null +++ b/pkg/kubernetes/clientset/mocks/stateful_set.go @@ -0,0 +1,297 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + client "sigs.k8s.io/controller-runtime/pkg/client" + + context "context" + + corev1 "k8s.io/api/core/v1" + + mock "github.com/stretchr/testify/mock" + + v1 "k8s.io/api/apps/v1" +) + +// StatefulSet is an autogenerated mock type for the StatefulSet type +type StatefulSet struct { + mock.Mock +} + +// CreateIfNotExistsStatefulSet provides a mock function with given fields: ctx, namespace, statefulSet +func (_m *StatefulSet) CreateIfNotExistsStatefulSet(ctx context.Context, namespace string, statefulSet *v1.StatefulSet) error { + ret := _m.Called(ctx, namespace, statefulSet) + + if len(ret) == 0 { + panic("no return value specified for CreateIfNotExistsStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, statefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateOrUpdateStatefulSet provides a mock function with given fields: ctx, namespace, StatefulSet +func (_m *StatefulSet) CreateOrUpdateStatefulSet(ctx context.Context, namespace string, StatefulSet *v1.StatefulSet) error { + ret := _m.Called(ctx, namespace, StatefulSet) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, StatefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateStatefulSet provides a mock function with given fields: ctx, namespace, statefulSet +func (_m *StatefulSet) CreateStatefulSet(ctx context.Context, namespace string, statefulSet *v1.StatefulSet) error { + ret := _m.Called(ctx, namespace, statefulSet) + + if len(ret) == 0 { + panic("no return value specified for CreateStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, statefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteStatefulSet provides a mock function with given fields: ctx, namespace, name, opts +func (_m *StatefulSet) DeleteStatefulSet(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, namespace, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...client.DeleteOption) error); ok { + r0 = rf(ctx, namespace, name, opts...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetStatefulSet provides a mock function with given fields: ctx, namespace, name +func (_m *StatefulSet) GetStatefulSet(ctx context.Context, namespace string, name string) (*v1.StatefulSet, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetStatefulSet") + } + + var r0 *v1.StatefulSet + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.StatefulSet, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.StatefulSet); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.StatefulSet) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStatefulSetPods provides a mock function with given fields: ctx, namespace, name +func (_m *StatefulSet) GetStatefulSetPods(ctx context.Context, namespace string, name string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetStatefulSetPods") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.PodList); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStatefulSetPodsByLabels provides a mock function with given fields: ctx, namespace, labels +func (_m *StatefulSet) GetStatefulSetPodsByLabels(ctx context.Context, namespace string, labels map[string]string) (*corev1.PodList, error) { + ret := _m.Called(ctx, namespace, labels) + + if len(ret) == 0 { + panic("no return value specified for GetStatefulSetPodsByLabels") + } + + var r0 *corev1.PodList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*corev1.PodList, error)); ok { + return rf(ctx, namespace, labels) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *corev1.PodList); ok { + r0 = rf(ctx, namespace, labels) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*corev1.PodList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, labels) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStatefulSetByLabels provides a mock function with given fields: ctx, namespace, labels +func (_m *StatefulSet) ListStatefulSetByLabels(ctx context.Context, namespace string, labels map[string]string) (*v1.StatefulSetList, error) { + ret := _m.Called(ctx, namespace, labels) + + if len(ret) == 0 { + panic("no return value specified for ListStatefulSetByLabels") + } + + var r0 *v1.StatefulSetList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) (*v1.StatefulSetList, error)); ok { + return rf(ctx, namespace, labels) + } + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]string) *v1.StatefulSetList); ok { + r0 = rf(ctx, namespace, labels) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.StatefulSetList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, map[string]string) error); ok { + r1 = rf(ctx, namespace, labels) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStatefulSets provides a mock function with given fields: ctx, namespace +func (_m *StatefulSet) ListStatefulSets(ctx context.Context, namespace string) (*v1.StatefulSetList, error) { + ret := _m.Called(ctx, namespace) + + if len(ret) == 0 { + panic("no return value specified for ListStatefulSets") + } + + var r0 *v1.StatefulSetList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.StatefulSetList, error)); ok { + return rf(ctx, namespace) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.StatefulSetList); ok { + r0 = rf(ctx, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.StatefulSetList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, namespace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateStatefulSet provides a mock function with given fields: ctx, namespace, statefulSet +func (_m *StatefulSet) UpdateStatefulSet(ctx context.Context, namespace string, statefulSet *v1.StatefulSet) error { + ret := _m.Called(ctx, namespace, statefulSet) + + if len(ret) == 0 { + panic("no return value specified for UpdateStatefulSet") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *v1.StatefulSet) error); ok { + r0 = rf(ctx, namespace, statefulSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewStatefulSet creates a new instance of StatefulSet. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStatefulSet(t interface { + mock.TestingT + Cleanup(func()) +}) *StatefulSet { + mock := &StatefulSet{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kubernetes/clientset/namespaces.go b/pkg/kubernetes/clientset/namespaces.go index 3e5138e..c868c22 100644 --- a/pkg/kubernetes/clientset/namespaces.go +++ b/pkg/kubernetes/clientset/namespaces.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/kubernetes/clientset/nodes.go b/pkg/kubernetes/clientset/nodes.go index ab02ff1..2032e16 100644 --- a/pkg/kubernetes/clientset/nodes.go +++ b/pkg/kubernetes/clientset/nodes.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/kubernetes/clientset/pod.go b/pkg/kubernetes/clientset/pod.go index 591490d..9ce8167 100644 --- a/pkg/kubernetes/clientset/pod.go +++ b/pkg/kubernetes/clientset/pod.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -21,9 +21,11 @@ import ( "context" "fmt" "io" + "net/http" "strings" "github.com/go-logr/logr" + "github.com/samber/lo" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,7 +34,7 @@ import ( "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" - "k8s.io/utils/pointer" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" ) @@ -48,7 +50,7 @@ type Pod interface { // CreateOrUpdatePod will update the given pod or create it if does not exist CreateOrUpdatePod(ctx context.Context, namespace string, pod *corev1.Pod) error // DeletePod will delete the given pod - DeletePod(ctx context.Context, namespace string, name string, force ...bool) error + DeletePod(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error // ListPodByLabels ListPodByLabels(ctx context.Context, namespace string, label_map map[string]string) (*corev1.PodList, error) // ListPods get set of pod on a given namespace @@ -76,13 +78,9 @@ func NewPod(kubeClient client.Client, restConfig *rest.Config, logger logr.Logge client: kubeClient, logger: logger, } - httpClient, err := rest.HTTPClientFor(restConfig) - if err != nil { - panic(err) - } if restConfig != nil { - restClient, err := apiutil.RESTClientForGVK(corev1.SchemeGroupVersion.WithKind("Pod"), false, restConfig, scheme.Codecs, httpClient) + restClient, err := apiutil.RESTClientForGVK(corev1.SchemeGroupVersion.WithKind("Pod"), false, restConfig, scheme.Codecs, http.DefaultClient) if err != nil { panic(err) } @@ -111,18 +109,24 @@ func (p *PodOption) CreatePod(ctx context.Context, namespace string, pod *corev1 return err } - p.logger.WithValues("namespace", namespace, "pod", pod.Name).Info("pod created") + p.logger.WithValues("namespace", namespace, "pod", pod.Name).V(3).Info("pod created") return nil } -// UpdatePod implement the Pod.Interface +// UpdatePod only overwrite labels/annotations of the pod func (p *PodOption) UpdatePod(ctx context.Context, namespace string, pod *corev1.Pod) error { - err := p.client.Update(ctx, pod) - if err != nil { - return err - } - p.logger.WithValues("namespace", namespace, "pod", pod.Name).Info("pod updated") - return nil + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldPod, err := p.GetPod(ctx, namespace, pod.Name) + if errors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + oldPod.Labels = lo.Assign(oldPod.Labels, pod.Labels) + oldPod.Annotations = lo.Assign(oldPod.Annotations, pod.Annotations) + + return p.client.Update(ctx, pod) + }) } // CreateOrUpdatePod implement the Pod.Interface @@ -145,7 +149,7 @@ func (p *PodOption) CreateOrUpdatePod(ctx context.Context, namespace string, pod } // DeletePod implement the Pod.Interface -func (p *PodOption) DeletePod(ctx context.Context, namespace string, name string, force ...bool) error { +func (p *PodOption) DeletePod(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error { pod := &corev1.Pod{} if err := p.client.Get(ctx, types.NamespacedName{ Name: name, @@ -153,11 +157,7 @@ func (p *PodOption) DeletePod(ctx context.Context, namespace string, name string }, pod); err != nil { return err } - opts := client.DeleteOptions{} - if len(force) > 0 && force[0] { - opts.GracePeriodSeconds = pointer.Int64(0) - } - return p.client.Delete(ctx, pod, &opts) + return p.client.Delete(ctx, pod, opts...) } // ListPods implement the Pod.Interface @@ -211,10 +211,8 @@ func (p *PodOption) Exec(ctx context.Context, namespace, name, containerName str var stdout, stderr bytes.Buffer if exec, err := remotecommand.NewSPDYExecutor(p.restConfig, "POST", req.URL()); err != nil { return nil, nil, err - } else { - if err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{Stdout: &stdout, Stderr: &stderr, Tty: true}); err != nil { - return nil, nil, err - } + } else if err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{Stdout: &stdout, Stderr: &stderr, Tty: true}); err != nil { + return nil, nil, err } return &stdout, &stderr, nil } diff --git a/pkg/kubernetes/clientset/poddisruptionbudget.go b/pkg/kubernetes/clientset/poddisruptionbudget.go index 68e85cb..9e71d8c 100644 --- a/pkg/kubernetes/clientset/poddisruptionbudget.go +++ b/pkg/kubernetes/clientset/poddisruptionbudget.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -73,7 +73,7 @@ func (p *PodDisruptionBudgetOption) CreatePodDisruptionBudget(ctx context.Contex if err != nil { return err } - p.logger.WithValues("namespace", namespace, "podDisruptionBudget", podDisruptionBudget.Name).Info("podDisruptionBudget created") + p.logger.WithValues("namespace", namespace, "podDisruptionBudget", podDisruptionBudget.Name).V(3).Info("podDisruptionBudget created") return nil } @@ -83,7 +83,7 @@ func (p *PodDisruptionBudgetOption) UpdatePodDisruptionBudget(ctx context.Contex if err != nil { return err } - p.logger.WithValues("namespace", namespace, "podDisruptionBudget", podDisruptionBudget.Name).Info("podDisruptionBudget updated") + p.logger.WithValues("namespace", namespace, "podDisruptionBudget", podDisruptionBudget.Name).V(3).Info("podDisruptionBudget updated") return nil } diff --git a/pkg/kubernetes/clientset/pvc.go b/pkg/kubernetes/clientset/pvc.go index 0c0bebf..8ffd0b7 100644 --- a/pkg/kubernetes/clientset/pvc.go +++ b/pkg/kubernetes/clientset/pvc.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -59,7 +59,7 @@ func (p *PVCService) CreatePVC(ctx context.Context, namespace string, pvc *corev if err := p.kubeClient.Create(ctx, pvc); err != nil { return err } - p.logger.Info("pvc created", "namespace", namespace, "pvc", pvc.Name) + p.logger.V(3).Info("pvc created", "namespace", namespace, "pvc", pvc.Name) return nil } diff --git a/pkg/kubernetes/clientset/rbac.go b/pkg/kubernetes/clientset/rbac.go index 1b5017e..8e21dbe 100644 --- a/pkg/kubernetes/clientset/rbac.go +++ b/pkg/kubernetes/clientset/rbac.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -74,7 +74,7 @@ func (s *RBACOption) UpdateClusterRole(ctx context.Context, role *rbacv1.Cluster if err != nil { return err } - s.logger.WithValues("ClusterRoleName", role.Name).Info("clusterRole updated") + s.logger.WithValues("ClusterRoleName", role.Name).V(3).Info("clusterRole updated") return nil } @@ -83,7 +83,7 @@ func (s *RBACOption) CreateClusterRole(ctx context.Context, role *rbacv1.Cluster if err != nil { return err } - s.logger.WithValues("ClusterRoleName", role.Name).Info("clusterRole created") + s.logger.WithValues("ClusterRoleName", role.Name).V(3).Info("clusterRole created") return nil } @@ -125,7 +125,7 @@ func (s *RBACOption) CreateClusterRoleBinding(ctx context.Context, rb *rbacv1.Cl if err != nil { return err } - s.logger.WithValues("ClusterRoleBindingName", rb.Name).Info("ClusterRoleBinding created") + s.logger.WithValues("ClusterRoleBindingName", rb.Name).V(3).Info("ClusterRoleBinding created") return nil } @@ -134,7 +134,7 @@ func (s *RBACOption) UpdateClusterRoleBinding(ctx context.Context, role *rbacv1. if err != nil { return err } - s.logger.WithValues("ClusterRoleBindingName", role.Name).Info("clusterRoleBinding updated") + s.logger.WithValues("ClusterRoleBindingName", role.Name).V(3).Info("clusterRoleBinding updated") return nil } @@ -189,7 +189,7 @@ func (s *RBACOption) CreateRole(ctx context.Context, namespace string, role *rba if err != nil { return err } - s.logger.WithValues("namespace", namespace, "roleName", role.Name).Info("role created") + s.logger.WithValues("namespace", namespace, "roleName", role.Name).V(3).Info("role created") return nil } @@ -198,7 +198,7 @@ func (s *RBACOption) UpdateRole(ctx context.Context, namespace string, role *rba if err != nil { return err } - s.logger.WithValues("namespace", namespace, "roleName", role.Name).Info("role updated") + s.logger.WithValues("namespace", namespace, "roleName", role.Name).V(3).Info("role updated") return nil } diff --git a/pkg/kubernetes/clientset/redisbackup.go b/pkg/kubernetes/clientset/redisbackup.go deleted file mode 100644 index f125b51..0000000 --- a/pkg/kubernetes/clientset/redisbackup.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package clientset - -import ( - "context" - - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - redisbackup "github.com/alauda/redis-operator/api/redis/v1" -) - -type RedisBackup interface { - GetRedisBackup(ctx context.Context, namespace string, name string) (*redisbackup.RedisBackup, error) - ListRedisBackups(ctx context.Context, namespace string, listOps client.ListOptions) (*redisbackup.RedisBackupList, error) - UpdateRedisBackup(ctx context.Context, backup *redisbackup.RedisBackup) error - UpdateRedisBackupStatus(ctx context.Context, backup *redisbackup.RedisBackup) error - DeleteRedisBackup(ctx context.Context, namespace string, name string) error -} - -type RedisBackupOption struct { - client client.Client - logger logr.Logger -} - -func NewRedisBackup(kubeClient client.Client, logger logr.Logger) RedisBackup { - logger = logger.WithValues("service", "k8s.RedisBackup") - return &RedisBackupOption{ - client: kubeClient, - logger: logger, - } -} - -func (r *RedisBackupOption) GetRedisBackup(ctx context.Context, namespace string, name string) (*redisbackup.RedisBackup, error) { - redis_backup := &redisbackup.RedisBackup{} - err := r.client.Get(ctx, types.NamespacedName{ - Name: name, - Namespace: namespace, - }, redis_backup) - - if err != nil { - return nil, err - } - return redis_backup, err -} - -func (r *RedisBackupOption) ListRedisBackups(ctx context.Context, namespace string, listOps client.ListOptions) (*redisbackup.RedisBackupList, error) { - rl := &redisbackup.RedisBackupList{} - err := r.client.List(ctx, rl, &listOps) - if err != nil { - return nil, err - } - return rl, err -} - -func (r *RedisBackupOption) UpdateRedisBackup(ctx context.Context, backup *redisbackup.RedisBackup) error { - if err := r.client.Update(ctx, backup); err != nil { - return err - } - r.logger.Info("redisbackup updated", "name", backup.Name) - - return nil -} - -// UpdateRedisBackup update redisbackup.Service interface. -func (r *RedisBackupOption) UpdateRedisBackupStatus(ctx context.Context, backup *redisbackup.RedisBackup) error { - if err := r.client.Status().Update(ctx, backup); err != nil { - return err - } - r.logger.Info("redisbackup status updated", "name", backup.Name) - - return nil -} - -func (r *RedisBackupOption) DeleteRedisBackup(ctx context.Context, namespace string, name string) error { - redis_backup := &redisbackup.RedisBackup{} - if err := r.client.Get(ctx, types.NamespacedName{ - Name: name, - Namespace: namespace, - }, redis_backup); err != nil { - return err - } - return r.client.Delete(ctx, redis_backup) -} diff --git a/pkg/kubernetes/clientset/redisclusterbackup.go b/pkg/kubernetes/clientset/redisclusterbackup.go deleted file mode 100644 index 076eef9..0000000 --- a/pkg/kubernetes/clientset/redisclusterbackup.go +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package clientset - -import ( - "context" - - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - redisbackup "github.com/alauda/redis-operator/api/redis/v1" -) - -type RedisClusterBackup interface { - GetRedisClusterBackup(ctx context.Context, namespace string, name string) (*redisbackup.RedisClusterBackup, error) - ListRedisClusterBackups(ctx context.Context, namespace string, listOps client.ListOptions) (*redisbackup.RedisClusterBackupList, error) - UpdateRedisClusterBackup(ctx context.Context, backup *redisbackup.RedisClusterBackup) error - UpdateRedisClusterBackupStatus(ctx context.Context, backup *redisbackup.RedisClusterBackup) error - DeleteRedisClusterBackup(ctx context.Context, namespace string, name string) error -} - -type RedisClusterBackupOption struct { - client client.Client - logger logr.Logger -} - -func NewRedisClusterBackup(kubeClient client.Client, logger logr.Logger) RedisClusterBackup { - logger = logger.WithValues("service", "k8s.RedisClusterBackup") - return &RedisClusterBackupOption{ - client: kubeClient, - logger: logger, - } -} - -func (r *RedisClusterBackupOption) GetRedisClusterBackup(ctx context.Context, namespace string, name string) (*redisbackup.RedisClusterBackup, error) { - redis_backup := &redisbackup.RedisClusterBackup{} - if err := r.client.Get(ctx, types.NamespacedName{ - Name: name, - Namespace: namespace, - }, redis_backup); err != nil { - return nil, err - } - return redis_backup, nil -} - -func (r *RedisClusterBackupOption) ListRedisClusterBackups(ctx context.Context, namespace string, listOps client.ListOptions) (*redisbackup.RedisClusterBackupList, error) { - rl := &redisbackup.RedisClusterBackupList{} - err := r.client.List(ctx, rl, &listOps) - if err != nil { - return nil, err - } - return rl, err -} - -func (r *RedisClusterBackupOption) UpdateRedisClusterBackup(ctx context.Context, backup *redisbackup.RedisClusterBackup) error { - if err := r.client.Update(ctx, backup); err != nil { - return err - } - r.logger.Info("redisclusterbackup updated", "name", backup.Name) - - return nil -} - -// UpdateRedisClusterBackup update redisbackup.Service interface. -func (r *RedisClusterBackupOption) UpdateRedisClusterBackupStatus(ctx context.Context, backup *redisbackup.RedisClusterBackup) error { - if err := r.client.Status().Update(ctx, backup); err != nil { - return err - } - r.logger.Info("redisclusterbackup status updated", "name", backup.Name) - - return nil -} - -func (r *RedisClusterBackupOption) DeleteRedisClusterBackup(ctx context.Context, namespace string, name string) error { - redis_backup := &redisbackup.RedisClusterBackup{} - if err := r.client.Get(ctx, types.NamespacedName{ - Name: name, - Namespace: namespace, - }, redis_backup); err != nil { - return err - } - return r.client.Delete(ctx, redis_backup) -} diff --git a/pkg/kubernetes/clientset/redisfailover.go b/pkg/kubernetes/clientset/redisfailover.go index 76e4651..5d25a66 100644 --- a/pkg/kubernetes/clientset/redisfailover.go +++ b/pkg/kubernetes/clientset/redisfailover.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,22 +18,26 @@ package clientset import ( "context" + "reflect" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" - redisfailoverv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" ) // RedisFailover the RF service that knows how to interact with k8s to get them type RedisFailover interface { // ListRedisFailovers lists the redisfailovers on a cluster. - ListRedisFailovers(ctx context.Context, namespace string, opts client.ListOptions) (*redisfailoverv1.RedisFailoverList, error) + ListRedisFailovers(ctx context.Context, namespace string, opts client.ListOptions) (*databasesv1.RedisFailoverList, error) // GetRedisFailover get the redisfailover on a cluster. - GetRedisFailover(ctx context.Context, namespace, name string) (*redisfailoverv1.RedisFailover, error) + GetRedisFailover(ctx context.Context, namespace, name string) (*databasesv1.RedisFailover, error) // UpdateRedisFailover update the redisfailover on a cluster. - UpdateRedisFailover(ctx context.Context, rf *redisfailoverv1.RedisFailover) error + UpdateRedisFailover(ctx context.Context, inst *databasesv1.RedisFailover) error + // UpdateRedisFailoverStatus + UpdateRedisFailoverStatus(ctx context.Context, inst *databasesv1.RedisFailover) error } // RedisFailoverService is the RedisFailover service implementation using API calls to kubernetes. @@ -44,7 +48,7 @@ type RedisFailoverService struct { // NewRedisFailoverService returns a new Workspace KubeService. func NewRedisFailoverService(client client.Client, logger logr.Logger) *RedisFailoverService { - logger = logger.WithName("k8s.redisfailover") + logger = logger.WithName("RedisFailover") return &RedisFailoverService{ client: client, @@ -53,8 +57,8 @@ func NewRedisFailoverService(client client.Client, logger logr.Logger) *RedisFai } // ListRedisFailovers satisfies redisfailover.Service interface. -func (r *RedisFailoverService) ListRedisFailovers(ctx context.Context, namespace string, opts client.ListOptions) (*redisfailoverv1.RedisFailoverList, error) { - ret := redisfailoverv1.RedisFailoverList{} +func (r *RedisFailoverService) ListRedisFailovers(ctx context.Context, namespace string, opts client.ListOptions) (*databasesv1.RedisFailoverList, error) { + ret := databasesv1.RedisFailoverList{} err := r.client.List(ctx, &ret, &opts) if err != nil { return nil, err @@ -63,8 +67,8 @@ func (r *RedisFailoverService) ListRedisFailovers(ctx context.Context, namespace } // GetRedisFailover satisfies redisfailover.Service interface. -func (r *RedisFailoverService) GetRedisFailover(ctx context.Context, namespace, name string) (*redisfailoverv1.RedisFailover, error) { - ret := redisfailoverv1.RedisFailover{} +func (r *RedisFailoverService) GetRedisFailover(ctx context.Context, namespace, name string) (*databasesv1.RedisFailover, error) { + ret := databasesv1.RedisFailover{} err := r.client.Get(ctx, types.NamespacedName{ Name: name, Namespace: namespace, @@ -76,31 +80,35 @@ func (r *RedisFailoverService) GetRedisFailover(ctx context.Context, namespace, } // UpdateRedisFailover -func (r *RedisFailoverService) UpdateRedisFailover(ctx context.Context, n *redisfailoverv1.RedisFailover) error { - o := redisfailoverv1.RedisFailover{} - err := r.client.Get(ctx, types.NamespacedName{ - Name: n.Name, - Namespace: n.Namespace, - }, &o) - if err != nil { - return err - } +func (r *RedisFailoverService) UpdateRedisFailover(ctx context.Context, inst *databasesv1.RedisFailover) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst databasesv1.RedisFailover + if err := r.client.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + r.logger.Error(err, "get RedisFailover failed") + return err + } + inst.ResourceVersion = oldInst.ResourceVersion + return r.client.Update(ctx, inst) + }) +} - o.Spec = n.Spec - o.Status = n.Status - if err := r.client.Update(ctx, &o); err != nil { - r.logger.Error(err, "update redis failover failed") - return err - } - if err := r.client.Status().Update(ctx, &o); err != nil { - r.logger.Error(err, "update redis failover status failed") - return err - } - return err +func (r *RedisFailoverService) UpdateRedisFailoverStatus(ctx context.Context, inst *databasesv1.RedisFailover) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst databasesv1.RedisFailover + if err := r.client.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + r.logger.Error(err, "get RedisFailover failed") + return err + } + if !reflect.DeepEqual(oldInst.Status, inst.Status) { + inst.ResourceVersion = oldInst.ResourceVersion + return r.client.Status().Update(ctx, inst) + } + return nil + }) } func (r *RedisFailoverService) DeleteRedisFailover(ctx context.Context, namespace string, name string) error { - ret := redisfailoverv1.RedisFailover{} + ret := databasesv1.RedisFailover{} if err := r.client.Get(ctx, types.NamespacedName{ Name: name, Namespace: namespace, diff --git a/pkg/kubernetes/clientset/redissentinel.go b/pkg/kubernetes/clientset/redissentinel.go new file mode 100644 index 0000000..9bd63ad --- /dev/null +++ b/pkg/kubernetes/clientset/redissentinel.go @@ -0,0 +1,115 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clientset + +import ( + "context" + "reflect" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" +) + +// RedisSentinel the sen service that knows how to interact with k8s to get them +type RedisSentinel interface { + // ListRedisSentinels lists the redisfailovers on a cluster. + ListRedisSentinels(ctx context.Context, namespace string, opts client.ListOptions) (*databasesv1.RedisSentinelList, error) + // GetRedisSentinel get the redisfailover on a cluster. + GetRedisSentinel(ctx context.Context, namespace, name string) (*databasesv1.RedisSentinel, error) + // UpdateRedisSentinel update the redisfailover on a cluster. + UpdateRedisSentinel(ctx context.Context, sen *databasesv1.RedisSentinel) error + // UpdateRedisSentinelStatus + UpdateRedisSentinelStatus(ctx context.Context, inst *databasesv1.RedisSentinel) error +} + +// RedisSentinelService is the RedisSentinel service implementation using API calls to kubernetes. +type RedisSentinelService struct { + client client.Client + logger logr.Logger +} + +// NewRedisSentinelService returns a new Workspace KubeService. +func NewRedisSentinelService(client client.Client, logger logr.Logger) *RedisSentinelService { + logger = logger.WithName("k8s.redisfailover") + + return &RedisSentinelService{ + client: client, + logger: logger, + } +} + +// ListRedisSentinels satisfies redisfailover.Service interface. +func (r *RedisSentinelService) ListRedisSentinels(ctx context.Context, namespace string, opts client.ListOptions) (*databasesv1.RedisSentinelList, error) { + ret := databasesv1.RedisSentinelList{} + if err := r.client.List(ctx, &ret, &opts); err != nil { + return nil, err + } + return &ret, nil +} + +// GetRedisSentinel satisfies redisfailover.Service interface. +func (r *RedisSentinelService) GetRedisSentinel(ctx context.Context, namespace, name string) (*databasesv1.RedisSentinel, error) { + ret := databasesv1.RedisSentinel{} + err := r.client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &ret) + if err != nil { + return nil, err + } + return &ret, nil +} + +// UpdateRedisSentinel +func (r *RedisSentinelService) UpdateRedisSentinel(ctx context.Context, inst *databasesv1.RedisSentinel) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst databasesv1.RedisSentinel + if err := r.client.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + r.logger.Error(err, "get RedisSentinel failed") + return err + } + inst.ResourceVersion = oldInst.ResourceVersion + return r.client.Update(ctx, inst) + }) +} + +func (r *RedisSentinelService) UpdateRedisSentinelStatus(ctx context.Context, inst *databasesv1.RedisSentinel) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var oldInst databasesv1.RedisSentinel + if err := r.client.Get(ctx, client.ObjectKeyFromObject(inst), &oldInst); err != nil { + r.logger.Error(err, "get RedisSentinel failed") + return err + } + if !reflect.DeepEqual(oldInst.Status, inst.Status) { + inst.ResourceVersion = oldInst.ResourceVersion + return r.client.Status().Update(ctx, inst) + } + return nil + }) +} + +func (r *RedisSentinelService) DeleteRedisSentinel(ctx context.Context, namespace string, name string) error { + ret := databasesv1.RedisSentinel{} + if err := r.client.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: namespace, + }, &ret); err != nil { + return err + } + return r.client.Delete(ctx, &ret) +} diff --git a/pkg/kubernetes/clientset/redisuser.go b/pkg/kubernetes/clientset/redisuser.go index 78788ef..1040f91 100644 --- a/pkg/kubernetes/clientset/redisuser.go +++ b/pkg/kubernetes/clientset/redisuser.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,28 +19,27 @@ package clientset import ( "context" + redisv1 "github.com/alauda/redis-operator/api/middleware/redis/v1" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - - redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/redis/v1" ) type RedisUser interface { // ListRedisUsers lists the redisusers on a cluster. - ListRedisUsers(ctx context.Context, namespace string, opts client.ListOptions) (*redismiddlewarealaudaiov1.RedisUserList, error) + ListRedisUsers(ctx context.Context, namespace string, opts client.ListOptions) (*redisv1.RedisUserList, error) // GetRedisUser get the redisuser on a cluster. - GetRedisUser(ctx context.Context, namespace, name string) (*redismiddlewarealaudaiov1.RedisUser, error) + GetRedisUser(ctx context.Context, namespace, name string) (*redisv1.RedisUser, error) // UpdateRedisUser update the redisuser on a cluster. - UpdateRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error + UpdateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error // Create - CreateRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error + CreateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error //create if not exites - CreateIfNotExistsRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error + CreateIfNotExistsRedisUser(ctx context.Context, ru *redisv1.RedisUser) error //create or update - CreateOrUpdateRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error + CreateOrUpdateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error } type RedisUserOption struct { @@ -56,8 +55,8 @@ func NewRedisUserService(client client.Client, logger logr.Logger) *RedisUserOpt } } -func (r *RedisUserOption) ListRedisUsers(ctx context.Context, namespace string, opts client.ListOptions) (*redismiddlewarealaudaiov1.RedisUserList, error) { - ret := redismiddlewarealaudaiov1.RedisUserList{} +func (r *RedisUserOption) ListRedisUsers(ctx context.Context, namespace string, opts client.ListOptions) (*redisv1.RedisUserList, error) { + ret := redisv1.RedisUserList{} err := r.client.List(ctx, &ret, &opts) if err != nil { return nil, err @@ -65,8 +64,8 @@ func (r *RedisUserOption) ListRedisUsers(ctx context.Context, namespace string, return &ret, nil } -func (r *RedisUserOption) GetRedisUser(ctx context.Context, namespace, name string) (*redismiddlewarealaudaiov1.RedisUser, error) { - ret := redismiddlewarealaudaiov1.RedisUser{} +func (r *RedisUserOption) GetRedisUser(ctx context.Context, namespace, name string) (*redisv1.RedisUser, error) { + ret := redisv1.RedisUser{} err := r.client.Get(ctx, types.NamespacedName{ Name: name, Namespace: namespace, @@ -77,8 +76,8 @@ func (r *RedisUserOption) GetRedisUser(ctx context.Context, namespace, name stri return &ret, nil } -func (r *RedisUserOption) UpdateRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error { - o := redismiddlewarealaudaiov1.RedisUser{} +func (r *RedisUserOption) UpdateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { + o := redisv1.RedisUser{} err := r.client.Get(ctx, types.NamespacedName{ Name: ru.Name, Namespace: ru.Namespace, @@ -86,7 +85,8 @@ func (r *RedisUserOption) UpdateRedisUser(ctx context.Context, ru *redismiddlewa if err != nil { return err } - + o.Annotations = ru.Annotations + o.Labels = ru.Labels o.Spec = ru.Spec o.Status = ru.Status if err := r.client.Update(ctx, &o); err != nil { @@ -101,7 +101,7 @@ func (r *RedisUserOption) UpdateRedisUser(ctx context.Context, ru *redismiddlewa } func (r *RedisUserOption) DeleteRedisUser(ctx context.Context, namespace string, name string) error { - ret := redismiddlewarealaudaiov1.RedisUser{} + ret := redisv1.RedisUser{} if err := r.client.Get(ctx, types.NamespacedName{ Name: name, Namespace: namespace, @@ -114,14 +114,14 @@ func (r *RedisUserOption) DeleteRedisUser(ctx context.Context, namespace string, return nil } -func (r *RedisUserOption) CreateRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error { +func (r *RedisUserOption) CreateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { if err := r.client.Create(ctx, ru); err != nil { return err } return nil } -func (r *RedisUserOption) CreateIfNotExistsRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error { +func (r *RedisUserOption) CreateIfNotExistsRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { if _, err := r.GetRedisUser(ctx, ru.Namespace, ru.Name); err != nil { if errors.IsNotFound(err) { return r.CreateRedisUser(ctx, ru) @@ -131,7 +131,7 @@ func (r *RedisUserOption) CreateIfNotExistsRedisUser(ctx context.Context, ru *re return nil } -func (r *RedisUserOption) CreateOrUpdateRedisUser(ctx context.Context, ru *redismiddlewarealaudaiov1.RedisUser) error { +func (r *RedisUserOption) CreateOrUpdateRedisUser(ctx context.Context, ru *redisv1.RedisUser) error { if oldRu, err := r.GetRedisUser(ctx, ru.Namespace, ru.Name); err != nil { if errors.IsNotFound(err) { return r.CreateRedisUser(ctx, ru) diff --git a/pkg/kubernetes/clientset/secret.go b/pkg/kubernetes/clientset/secret.go index 460ea75..2f5c3f5 100644 --- a/pkg/kubernetes/clientset/secret.go +++ b/pkg/kubernetes/clientset/secret.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/kubernetes/clientset/service.go b/pkg/kubernetes/clientset/service.go index 44f9857..d4f433d 100644 --- a/pkg/kubernetes/clientset/service.go +++ b/pkg/kubernetes/clientset/service.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" "github.com/go-logr/logr" @@ -48,6 +49,7 @@ type Service interface { ListServices(ctx context.Context, namespace string) (*corev1.ServiceList, error) GetServiceByLabels(ctx context.Context, namespace string, labelsMap map[string]string) (*corev1.ServiceList, error) UpdateIfSelectorChangedService(ctx context.Context, namespace string, service *corev1.Service) error + CreateOrUpdateIfServiceChanged(ctx context.Context, namespace string, service *corev1.Service) error } // ServiceOption is the service client implementation using API calls to kubernetes. @@ -102,7 +104,7 @@ func (s *ServiceOption) CreateService(ctx context.Context, namespace string, ser if err != nil { return err } - s.logger.WithValues("namespace", namespace, "serviceName", service.Name).Info("service created") + s.logger.WithValues("namespace", namespace, "serviceName", service.Name).V(3).Info("service created") return nil } @@ -118,47 +120,59 @@ func (s *ServiceOption) CreateIfNotExistsService(ctx context.Context, namespace return nil } -func (s *ServiceOption) UpdateIfSelectorChangedService(ctx context.Context, namespace string, service *corev1.Service) error { - old_service, err := s.GetService(ctx, namespace, service.Name) - if err != nil { - // If no resource we need to create. - if errors.IsNotFound(err) { - return s.CreateService(ctx, namespace, service) - } +// CreateOrUpdateIfServiceChanged implement the Service.Interface +func (s *ServiceOption) CreateOrUpdateIfServiceChanged(ctx context.Context, namespace string, service *corev1.Service) error { + oldSvc, err := s.GetService(ctx, namespace, service.Name) + if errors.IsNotFound(err) { + return s.CreateService(ctx, namespace, service) + } else if err != nil { return err } - if !reflect.DeepEqual(old_service.Spec.Selector, service.Spec.Selector) { + if !reflect.DeepEqual(oldSvc.Labels, service.Labels) || + !reflect.DeepEqual(oldSvc.Spec.Selector, service.Spec.Selector) || + len(oldSvc.Spec.Ports) != len(service.Spec.Ports) { + return s.UpdateService(ctx, namespace, service) } return nil } -// UpdateService implement the Service.Interface -func (s *ServiceOption) UpdateService(ctx context.Context, namespace string, service *corev1.Service) error { - err := s.client.Update(ctx, service) - if err != nil { +func (s *ServiceOption) UpdateIfSelectorChangedService(ctx context.Context, namespace string, service *corev1.Service) error { + oldSvc, err := s.GetService(ctx, namespace, service.Name) + if errors.IsNotFound(err) { + return s.CreateService(ctx, namespace, service) + } else if err != nil { return err } - s.logger.WithValues("namespace", namespace, "serviceName", service.Name).Info("service updated") + if !reflect.DeepEqual(oldSvc.Spec.Selector, service.Spec.Selector) { + return s.UpdateService(ctx, namespace, service) + } return nil } -// CreateOrUpdateService implement the Service.Interface -func (s *ServiceOption) CreateOrUpdateService(ctx context.Context, namespace string, service *corev1.Service) error { - storedService, err := s.GetService(ctx, namespace, service.Name) - if err != nil { - // If no resource we need to create. +// UpdateService implement the Service.Interface +func (s *ServiceOption) UpdateService(ctx context.Context, namespace string, service *corev1.Service) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldSvc, err := s.GetService(ctx, namespace, service.Name) if errors.IsNotFound(err) { - return s.CreateService(ctx, namespace, service) + return nil + } else if err != nil { + return err } + service.ResourceVersion = oldSvc.ResourceVersion + return s.client.Update(ctx, service) + }) +} + +// CreateOrUpdateService implement the Service.Interface +func (s *ServiceOption) CreateOrUpdateService(ctx context.Context, namespace string, service *corev1.Service) error { + oldSvc, err := s.GetService(ctx, namespace, service.Name) + if errors.IsNotFound(err) { + return s.CreateService(ctx, namespace, service) + } else if err != nil { return err } - - // Already exists, need to Update. - // Set the correct resource version to ensure we are on the latest version. This way the only valid - // namespace is our spec(https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency), - // we will replace the current namespace state. - service.ResourceVersion = storedService.ResourceVersion + service.ResourceVersion = oldSvc.ResourceVersion return s.UpdateService(ctx, namespace, service) } @@ -171,7 +185,10 @@ func (s *ServiceOption) DeleteService(ctx context.Context, namespace string, nam }, service); err != nil { return err } - return s.client.Delete(ctx, service) + + err := s.client.Delete(ctx, service) + s.logger.WithValues("namespace", namespace, "serviceName", service.Name).V(3).Info("service deleted") + return err } // ListServices implement the Service.Interface diff --git a/pkg/kubernetes/clientset/serviceaccount.go b/pkg/kubernetes/clientset/serviceaccount.go index b43447d..f3f2b84 100644 --- a/pkg/kubernetes/clientset/serviceaccount.go +++ b/pkg/kubernetes/clientset/serviceaccount.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -63,7 +63,7 @@ func (s *ServiceAccountOption) CreateServiceAccount(ctx context.Context, namespa if err != nil { return err } - s.logger.WithValues("namespace", namespace, "serviceAccountName", sa.Name).Info("serviceAccount created") + s.logger.WithValues("namespace", namespace, "serviceAccountName", sa.Name).V(3).Info("serviceAccount created") return nil } diff --git a/pkg/kubernetes/clientset/servicemonitor.go b/pkg/kubernetes/clientset/servicemonitor.go index 72b6440..0f7d169 100644 --- a/pkg/kubernetes/clientset/servicemonitor.go +++ b/pkg/kubernetes/clientset/servicemonitor.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,17 +20,17 @@ import ( "context" "github.com/go-logr/logr" - v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + monitoring "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) type ServiceMonitor interface { - GetServiceMonitor(ctx context.Context, namespace string, name string) (*v1.ServiceMonitor, error) - CreateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error - CreateOrUpdateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error - UpdateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error + GetServiceMonitor(ctx context.Context, namespace string, name string) (*monitoring.ServiceMonitor, error) + CreateServiceMonitor(ctx context.Context, namespace string, sm *monitoring.ServiceMonitor) error + CreateOrUpdateServiceMonitor(ctx context.Context, namespace string, sm *monitoring.ServiceMonitor) error + UpdateServiceMonitor(ctx context.Context, namespace string, sm *monitoring.ServiceMonitor) error } type ServiceMonitorOption struct { @@ -46,8 +46,8 @@ func NewServiceMonitor(kubeClient client.Client, logger logr.Logger) ServiceMoni } } -func (s *ServiceMonitorOption) GetServiceMonitor(ctx context.Context, namespace string, name string) (*v1.ServiceMonitor, error) { - sm := &v1.ServiceMonitor{} +func (s *ServiceMonitorOption) GetServiceMonitor(ctx context.Context, namespace string, name string) (*monitoring.ServiceMonitor, error) { + sm := &monitoring.ServiceMonitor{} err := s.client.Get(ctx, types.NamespacedName{ Name: name, Namespace: namespace, @@ -59,17 +59,17 @@ func (s *ServiceMonitorOption) GetServiceMonitor(ctx context.Context, namespace return sm, err } -func (s *ServiceMonitorOption) CreateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error { +func (s *ServiceMonitorOption) CreateServiceMonitor(ctx context.Context, namespace string, sm *monitoring.ServiceMonitor) error { sm.Namespace = namespace err := s.client.Create(ctx, sm) if err != nil { return err } - s.logger.WithValues("namespace", namespace, "ServiceMonitor", sm.Name).Info("ServiceMonitor created") + s.logger.WithValues("namespace", namespace, "ServiceMonitor", sm.Name).V(3).Info("ServiceMonitor created") return nil } -func (s *ServiceMonitorOption) CreateOrUpdateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error { +func (s *ServiceMonitorOption) CreateOrUpdateServiceMonitor(ctx context.Context, namespace string, sm *monitoring.ServiceMonitor) error { storedSm, err := s.GetServiceMonitor(ctx, namespace, sm.Name) if err != nil { if errors.IsNotFound(err) { @@ -82,12 +82,12 @@ func (s *ServiceMonitorOption) CreateOrUpdateServiceMonitor(ctx context.Context, return s.UpdateServiceMonitor(ctx, namespace, sm) } -func (s *ServiceMonitorOption) UpdateServiceMonitor(ctx context.Context, namespace string, sm *v1.ServiceMonitor) error { +func (s *ServiceMonitorOption) UpdateServiceMonitor(ctx context.Context, namespace string, sm *monitoring.ServiceMonitor) error { sm.Namespace = namespace err := s.client.Update(ctx, sm) if err != nil { return err } - s.logger.WithValues("namespace", namespace, "ServiceMonitor", sm.Name).Info("ServiceMonitor updated") + s.logger.WithValues("namespace", namespace, "ServiceMonitor", sm.Name).V(3).Info("ServiceMonitor updated") return nil } diff --git a/pkg/kubernetes/clientset/statefulset.go b/pkg/kubernetes/clientset/statefulset.go index 70025d6..7d37509 100644 --- a/pkg/kubernetes/clientset/statefulset.go +++ b/pkg/kubernetes/clientset/statefulset.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -26,7 +26,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -45,7 +44,7 @@ type StatefulSet interface { // CreateOrUpdateStatefulSet will update the given StatefulSet or create it if does not exist CreateOrUpdateStatefulSet(ctx context.Context, namespace string, StatefulSet *appsv1.StatefulSet) error // DeleteStatefulSet will delete the given StatefulSet - DeleteStatefulSet(ctx context.Context, namespace string, name string, force ...bool) error + DeleteStatefulSet(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error // ListStatefulSets get set of StatefulSet on a given namespace ListStatefulSets(ctx context.Context, namespace string) (*appsv1.StatefulSetList, error) // ListStatefulsetByLabels @@ -111,7 +110,7 @@ func (s *StatefulSetOption) CreateStatefulSet(ctx context.Context, namespace str if err != nil { return err } - s.logger.WithValues("namespace", namespace, "statefulSet", statefulSet.ObjectMeta.Name).Info("statefulSet created") + s.logger.WithValues("namespace", namespace, "statefulSet", statefulSet.ObjectMeta.Name).V(3).Info("statefulSet created") return err } @@ -121,7 +120,7 @@ func (s *StatefulSetOption) UpdateStatefulSet(ctx context.Context, namespace str if err != nil { return err } - s.logger.WithValues("namespace", namespace, "statefulSet", statefulSet.ObjectMeta.Name).Info("statefulSet updated") + s.logger.WithValues("namespace", namespace, "statefulSet", statefulSet.ObjectMeta.Name).V(3).Info("statefulSet updated") return err } @@ -163,7 +162,7 @@ func (s *StatefulSetOption) CreateIfNotExistsStatefulSet(ctx context.Context, na } // DeleteStatefulSet implement the StatefulSet.Interface -func (s *StatefulSetOption) DeleteStatefulSet(ctx context.Context, namespace string, name string, force ...bool) error { +func (s *StatefulSetOption) DeleteStatefulSet(ctx context.Context, namespace string, name string, opts ...client.DeleteOption) error { statefulset := &appsv1.StatefulSet{} if err := s.client.Get(ctx, types.NamespacedName{ Name: name, @@ -171,11 +170,7 @@ func (s *StatefulSetOption) DeleteStatefulSet(ctx context.Context, namespace str }, statefulset); err != nil { return err } - opts := client.DeleteOptions{} - if len(force) > 0 && force[0] { - opts.GracePeriodSeconds = pointer.Int64(0) - } - return s.client.Delete(ctx, statefulset, &opts) + return s.client.Delete(ctx, statefulset, opts...) } // ListStatefulSets implement the StatefulSet.Interface diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index adb595a..5d7469e 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -39,9 +39,8 @@ type ClientSet interface { clientset.StatefulSet clientset.ServiceMonitor - clientset.RedisBackup - clientset.RedisClusterBackup clientset.RedisFailover + clientset.RedisSentinel clientset.DistributedRedisCluster clientset.Node clientset.RedisUser diff --git a/pkg/models/sentinel/nodes.go b/pkg/models/sentinel/nodes.go deleted file mode 100644 index 6c8559a..0000000 --- a/pkg/models/sentinel/nodes.go +++ /dev/null @@ -1,152 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinel - -import ( - "context" - "encoding/json" - "fmt" - "time" - - clientset "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/models" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - appv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/api/errors" - k8stypes "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type RedisSentinelNodes struct { - appv1.Deployment - client clientset.ClientSet - sentinel types.RedisInstance - nodes []redis.RedisNode - logger logr.Logger -} - -func LoadRedisSentinelNodes(ctx context.Context, client clientset.ClientSet, sentinel types.RedisInstance, logger logr.Logger) (types.RedisSentinelNodes, error) { - name := sentinelbuilder.GetSentinelDeploymentName(sentinel.GetName()) - var shard types.RedisSentinelNodes - deploy, err := client.GetDeployment(ctx, sentinel.GetNamespace(), name) - if err != nil { - if errors.IsNotFound(err) { - return shard, nil - } - logger.Info("load deployment failed", "name", name) - return nil, err - } - if shard, err = NewRedisSentinelNode(ctx, client, sentinel, deploy, logger); err != nil { - logger.Error(err, "parse shard failed") - } - return shard, nil -} - -func (s *RedisSentinelNodes) Version() redis.RedisVersion { - if s == nil { - return redis.RedisVersionUnknown - } - container := util.GetContainerByName(&s.Spec.Template.Spec, sentinelbuilder.ServerContainerName) - ver, _ := redis.ParseRedisVersionFromImage(container.Image) - return ver -} - -func NewRedisSentinelNode(ctx context.Context, client clientset.ClientSet, sentinel types.RedisInstance, deploy *appv1.Deployment, logger logr.Logger) (types.RedisSentinelNodes, error) { - if client == nil { - return nil, fmt.Errorf("require clientset") - } - if sentinel == nil { - return nil, fmt.Errorf("require sentinel instance") - } - if deploy == nil { - return nil, fmt.Errorf("require deployment") - } - - node := RedisSentinelNodes{ - Deployment: *deploy, - client: client, - sentinel: sentinel, - logger: logger.WithName("SentinelNode"), - } - user := sentinel.Users() - var err error - if node.nodes, err = models.LoadRedisSentinelNodes(ctx, client, deploy, user.GetOpUser(), logger); err != nil { - logger.Error(err, "load shard nodes failed", "shard", deploy.GetName()) - return nil, err - } - return &node, nil -} - -func (s *RedisSentinelNodes) Definition() *appv1.Deployment { - if s == nil { - return nil - } - return &s.Deployment -} - -func (s *RedisSentinelNodes) Nodes() []redis.RedisNode { - if s == nil { - return nil - } - return s.nodes -} - -func (s *RedisSentinelNodes) Restart(ctx context.Context) error { - // update all shards - logger := s.logger.WithName("Restart") - - data, _ := json.Marshal(map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "metadata": map[string]interface{}{ - "annotations": map[string]string{ - "kubectl.kubernetes.io/restartedAt": time.Now().Format(time.RFC3339Nano), - }, - }, - }, - }, - }) - - if err := s.client.Client().Patch(ctx, &s.Deployment, - client.RawPatch(k8stypes.StrategicMergePatchType, data)); err != nil { - logger.Error(err, "restart deployment failed", "target", client.ObjectKeyFromObject(&s.Deployment)) - return err - } - return nil -} - -func (s *RedisSentinelNodes) Refresh(ctx context.Context) error { - logger := s.logger.WithName("Refresh") - - var err error - if s.nodes, err = models.LoadRedisSentinelNodes(ctx, s.client, &s.Deployment, s.sentinel.Users().GetOpUser(), logger); err != nil { - logger.Error(err, "load shard nodes failed", "shard", s.GetName()) - return err - } - return nil -} - -func (s *RedisSentinelNodes) Status() *appv1.DeploymentStatus { - if s == nil { - return nil - } - return &s.Deployment.Status -} diff --git a/pkg/models/sentinel/sentinel.go b/pkg/models/sentinel/sentinel.go deleted file mode 100644 index 8de8325..0000000 --- a/pkg/models/sentinel/sentinel.go +++ /dev/null @@ -1,533 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinel - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "reflect" - "strconv" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - redismiddlewarealaudaiov1 "github.com/alauda/redis-operator/api/redis/v1" - clientset "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/security/acl" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/client-go/util/retry" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ types.RedisInstance = (*RedisFailover)(nil) - -type RedisFailover struct { - databasesv1.RedisFailover - redisUsers []*redismiddlewarealaudaiov1.RedisUser - ctx context.Context - client clientset.ClientSet - // version redis.RedisVersion - users acl.Users - tlsConfig *tls.Config - configmap map[string]string - replicas []types.RedisSentinelReplica - sentinel types.RedisSentinelNodes - logger logr.Logger - // selector map[string]string -} - -func NewRedisFailover(ctx context.Context, k8sClient clientset.ClientSet, def *databasesv1.RedisFailover, logger logr.Logger) (*RedisFailover, error) { - sentinel := &RedisFailover{ - RedisFailover: *def, - ctx: ctx, - client: k8sClient, - configmap: make(map[string]string), - logger: logger, - } - var err error - if sentinel.users, err = sentinel.loadUsers(ctx); err != nil { - sentinel.logger.Error(err, "load user failed") - return nil, err - } - if sentinel.tlsConfig, err = sentinel.loadTLS(ctx); err != nil { - sentinel.logger.Error(err, "loads tls failed") - return nil, err - } - if sentinel.replicas, err = LoadRedisSentinelReplicas(ctx, k8sClient, sentinel, logger); err != nil { - sentinel.logger.Error(err, "load replicas failed") - return nil, err - } - - if sentinel.sentinel, err = LoadRedisSentinelNodes(ctx, k8sClient, sentinel, logger); err != nil { - sentinel.logger.Error(err, "load sentinels failed") - return nil, err - } - - if sentinel.Version().IsACLSupported() { - sentinel.LoadRedisUsers(ctx) - } - - return sentinel, nil -} - -func (c *RedisFailover) UpdateStatus(ctx context.Context, status databasesv1.RedisFailoverStatus) error { - if c == nil { - return nil - } - count := 0 - - for _, r := range c.replicas { - for _, n := range r.Nodes() { - if n.Role() == redis.RedisRoleMaster { - status.Master.Name = n.GetName() - } - count += 1 - } - if r.Status().ReadyReplicas != c.Definition().Spec.Redis.Replicas { - if !(status.Phase == databasesv1.PhaseFail || status.Phase == databasesv1.PhasePaused) { - status.Phase = databasesv1.PhaseWaitingPodReady - } - } - } - status.Instance.Redis.Size = int32(count) - - if count != int(c.Definition().Spec.Redis.Replicas) { - if !(status.Phase == databasesv1.PhaseFail || status.Phase == databasesv1.PhasePaused) { - status.Phase = databasesv1.PhaseWaitingPodReady - } - } - count = 0 - status.Master.Status = databasesv1.RedisStatusMasterDown - if c.sentinel != nil { - for _, n := range c.sentinel.Nodes() { - if n.Info().SentinelMaster0.Status == "ok" { - status.Master.Address = n.Info().SentinelMaster0.Address.ToString() - status.Master.Status = databasesv1.RedisStatusMasterOK - status.Instance.Redis.Ready = int32(n.Info().SentinelMaster0.Replicas) + 1 - status.Instance.Sentinel.Ready = int32(n.Info().SentinelMaster0.Sentinels) - } - count += 1 - } - if c.sentinel.Status().ReadyReplicas != c.Definition().Spec.Sentinel.Replicas { - if !(status.Phase == databasesv1.PhaseFail || status.Phase == databasesv1.PhasePaused) { - status.Phase = databasesv1.PhaseWaitingPodReady - } - } - } - if count != int(c.Definition().Spec.Sentinel.Replicas) { - if !(status.Phase == databasesv1.PhaseFail || status.Phase == databasesv1.PhasePaused) { - status.Phase = databasesv1.PhasePending - } - } - if status.Instance.Redis.Ready != c.Definition().Spec.Redis.Replicas || - status.Instance.Sentinel.Ready != c.Definition().Spec.Sentinel.Replicas { - if !(status.Phase == databasesv1.PhaseFail || status.Phase == databasesv1.PhasePaused) { - status.Phase = databasesv1.PhaseWaitingPodReady - } - } - - status.Instance.Sentinel.Size = int32(count) - svcName := sentinelbuilder.GetSentinelServiceName(c.GetName()) - status.Instance.Sentinel.Service = svcName - if svc, err := c.client.GetService(ctx, c.GetNamespace(), svcName); err != nil { - if errors.IsNotFound(err) { - c.logger.Info("sen service not found") - } else { - return err - } - } else { - status.Instance.Sentinel.Port = strconv.Itoa(int(svc.Spec.Ports[0].Port)) - status.Instance.Sentinel.ClusterIP = svc.Spec.ClusterIP - } - - for _, repl := range c.replicas { - if repl.Status().CurrentRevision != repl.Status().UpdateRevision { - if !(status.Phase == databasesv1.PhaseFail || status.Phase == databasesv1.PhasePaused) { - status.Phase = databasesv1.PhaseWaitingPodReady - } - } - } - - if c.sentinel != nil && c.sentinel.Status().Replicas != c.sentinel.Status().UpdatedReplicas { - if !(status.Phase == databasesv1.PhaseFail || status.Phase == databasesv1.PhasePaused) { - status.Phase = databasesv1.PhaseWaitingPodReady - } - } - - if status.Phase == "" { - status.Phase = databasesv1.PhaseReady - } - - if !reflect.DeepEqual(c.RedisFailover.Status, status) { - c.RedisFailover.Status = status - return c.client.Client().Status().Update(ctx, &c.RedisFailover) - } - return nil - -} - -// common method - -func (c *RedisFailover) Definition() *databasesv1.RedisFailover { - if c == nil { - return nil - } - return &c.RedisFailover -} - -func (c *RedisFailover) Version() redis.RedisVersion { - if c == nil { - return redis.RedisVersionUnknown - } - - if version, err := redis.ParseRedisVersionFromImage(c.Spec.Redis.Image); err != nil { - c.logger.Error(err, "parse redis version failed") - return redis.RedisVersionUnknown - } else { - return version - } -} - -func (c *RedisFailover) Masters() []redis.RedisNode { - var ret []redis.RedisNode - for _, nodes := range c.replicas { - for _, v := range nodes.Nodes() { - if v.Role() == redis.RedisRoleMaster { - ret = append(ret, v) - } - } - } - return ret -} - -func (c *RedisFailover) Nodes() []redis.RedisNode { - var ret []redis.RedisNode - for _, v := range c.replicas { - ret = append(ret, v.Nodes()...) - } - return ret -} - -func (c *RedisFailover) SentinelNodes() []redis.RedisNode { - var ret []redis.RedisNode - if c.sentinel != nil { - ret = append(ret, c.sentinel.Nodes()...) - } - return ret -} - -func (c *RedisFailover) Sentinel() types.RedisSentinelNodes { - if c == nil { - return nil - } - return c.sentinel -} - -func (c *RedisFailover) IsInService() bool { - return c != nil -} - -func (c *RedisFailover) IsReady() bool { - if c == nil { - return false - } - if c.RedisFailover.Status.Phase == databasesv1.PhaseReady { - return true - } - return false -} - -func (c *RedisFailover) Users() (us acl.Users) { - if c == nil { - return nil - } - - // clone before return - for _, user := range c.users { - u := *user - if u.Password != nil { - p := *u.Password - u.Password = &p - } - us = append(us, &u) - } - return -} - -func (c *RedisFailover) Restart(ctx context.Context) error { - if c == nil { - return nil - } - for _, v := range c.replicas { - if err := v.Restart(ctx); err != nil { - return err - } - } - return nil -} - -func (c *RedisFailover) Refresh(ctx context.Context) error { - if c == nil { - return nil - } - logger := c.logger.WithName("Refresh") - logger.V(3).Info("refreshing sentinel", "target", fmt.Sprintf("%s/%s", c.GetNamespace(), c.GetName())) - - // load cr - var cr databasesv1.RedisFailover - if err := retry.OnError(retry.DefaultRetry, func(err error) bool { - if errors.IsInternalError(err) || - errors.IsServerTimeout(err) || - errors.IsTimeout(err) || - errors.IsTooManyRequests(err) || - errors.IsServiceUnavailable(err) { - return true - } - return false - }, func() error { - return c.client.Client().Get(ctx, client.ObjectKeyFromObject(&c.RedisFailover), &cr) - }); err != nil { - if errors.IsNotFound(err) { - return nil - } - logger.Error(err, "get RedisFailover failed") - return err - } - if cr.Name == "" { - return fmt.Errorf("RedisFailover is nil") - } - c.RedisFailover = cr - err := c.RedisFailover.Validate() - if err != nil { - return err - } - - if c.users, err = c.loadUsers(ctx); err != nil { - logger.Error(err, "load users failed") - return err - } - - if c.replicas, err = LoadRedisSentinelReplicas(ctx, c.client, c, logger); err != nil { - logger.Error(err, "load replicas failed") - return err - } - if c.sentinel, err = LoadRedisSentinelNodes(ctx, c.client, c, logger); err != nil { - logger.Error(err, "load sentinels failed") - return err - } - return nil -} - -func (c *RedisFailover) LoadRedisUsers(ctx context.Context) { - oldOpUser, _ := c.client.GetRedisUser(ctx, c.GetNamespace(), sentinelbuilder.GenerateSentinelOperatorsRedisUserName(c.GetName())) - oldDefultUser, _ := c.client.GetRedisUser(ctx, c.GetNamespace(), sentinelbuilder.GenerateSentinelDefaultRedisUserName(c.GetName())) - c.redisUsers = []*redismiddlewarealaudaiov1.RedisUser{oldOpUser, oldDefultUser} -} - -func (c *RedisFailover) loadUsers(ctx context.Context) (acl.Users, error) { - var ( - name = sentinelbuilder.GenerateSentinelACLConfigMapName(c.GetName()) - users acl.Users - ) - if cm, err := c.client.GetConfigMap(ctx, c.GetNamespace(), name); errors.IsNotFound(err) { - var ( - username string - passwordSecret string - secret *corev1.Secret - ) - statefulSetName := sentinelbuilder.GetSentinelStatefulSetName(c.GetName()) - sts, err := c.client.GetStatefulSet(ctx, c.GetNamespace(), statefulSetName) - if err != nil { - if !errors.IsNotFound(err) { - c.logger.Error(err, "load statefulset failed", "target", fmt.Sprintf("%s/%s", c.GetNamespace(), c.GetName())) - } - if c.Version().IsACLSupported() { - passwordSecret = sentinelbuilder.GenerateSentinelACLOperatorSecretName(c.GetName()) - username = user.DefaultOperatorUserName - } - } else { - spec := sts.Spec.Template.Spec - if container := util.GetContainerByName(&spec, sentinelbuilder.ServerContainerName); container != nil { - for _, env := range container.Env { - if env.Name == sentinelbuilder.PasswordENV && env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { - passwordSecret = env.ValueFrom.SecretKeyRef.LocalObjectReference.Name - } else if env.Name == sentinelbuilder.OperatorSecretName && env.Value != "" { - passwordSecret = env.Value - } else if env.Name == sentinelbuilder.OperatorUsername { - username = env.Value - } - } - } - } - - if passwordSecret != "" { - objKey := client.ObjectKey{Namespace: c.GetNamespace(), Name: passwordSecret} - if secret, err = c.loadUserSecret(ctx, objKey); err != nil { - c.logger.Error(err, "load user secret failed", "target", objKey) - return nil, err - } - } else if c.Spec.Auth.SecretPath != "" { - secret, err = c.client.GetSecret(ctx, c.GetNamespace(), c.Spec.Auth.SecretPath) - if err != nil { - return nil, err - } - } - role := user.RoleDeveloper - if username == user.DefaultOperatorUserName { - role = user.RoleOperator - } else if username == "" { - username = user.DefaultUserName - } - - if role == user.RoleOperator { - if u, err := user.NewOperatorUser(secret, c.Version().IsACL2Supported()); err != nil { - c.logger.Error(err, "init users failed") - return nil, err - } else { - users = append(users, u) - } - - if c.Spec.Auth.SecretPath != "" { - secret, err = c.client.GetSecret(ctx, c.GetNamespace(), c.Spec.Auth.SecretPath) - if err != nil { - return nil, err - } - u, _ := user.NewUser(user.DefaultUserName, user.RoleDeveloper, secret) - users = append(users, u) - } else { - u, _ := user.NewUser(user.DefaultUserName, user.RoleDeveloper, nil) - users = append(users, u) - } - } else { - if u, err := user.NewUser(username, role, secret); err != nil { - c.logger.Error(err, "init users failed") - return nil, err - } else { - users = append(users, u) - } - } - } else if err != nil { - c.logger.Error(err, "load default users's password secret failed", "target", fmt.Sprintf("%s/%s", c.GetNamespace(), name)) - return nil, err - } else if users, err = acl.LoadACLUsers(ctx, c.client, cm); err != nil { - c.logger.Error(err, "load acl failed") - return nil, err - } - return users, nil -} - -func (c *RedisFailover) loadUserSecret(ctx context.Context, objKey client.ObjectKey) (*corev1.Secret, error) { - secret, err := c.client.GetSecret(ctx, objKey.Namespace, objKey.Name) - if err != nil && !errors.IsNotFound(err) { - c.logger.Error(err, "load default users's password secret failed", "target", objKey.String()) - return nil, err - } else if errors.IsNotFound(err) { - secret = sentinelbuilder.NewSentinelOpSecret(c.Definition()) - err := c.client.CreateSecret(ctx, objKey.Namespace, secret) - if err != nil { - return nil, err - } - } else if _, ok := secret.Data[user.PasswordSecretKey]; !ok { - return nil, fmt.Errorf("no password found") - } - return secret, nil -} - -func (c *RedisFailover) loadTLS(ctx context.Context) (*tls.Config, error) { - if c == nil { - return nil, nil - } - logger := c.logger.WithName("loadTLS") - - var secretName string - - // load current tls secret. - // because previous cr not recorded the secret name, we should load it from statefulset - stsName := sentinelbuilder.GetSentinelStatefulSetName(c.GetName()) - if sts, err := c.client.GetStatefulSet(ctx, c.GetNamespace(), stsName); err != nil { - c.logger.Error(err, "load statefulset failed", "target", fmt.Sprintf("%s/%s", c.GetNamespace(), c.GetName())) - } else { - for _, vol := range sts.Spec.Template.Spec.Volumes { - if vol.Name == sentinelbuilder.RedisTLSVolumeName { - secretName = vol.VolumeSource.Secret.SecretName - } - } - } - - if secretName == "" { - return nil, nil - } - - if secret, err := c.client.GetSecret(ctx, c.GetNamespace(), secretName); err != nil { - logger.Error(err, "secret not found", "name", secretName) - return nil, err - } else if secret.Data[corev1.TLSCertKey] == nil || secret.Data[corev1.TLSPrivateKeyKey] == nil || - secret.Data["ca.crt"] == nil { - - logger.Error(fmt.Errorf("invalid tls secret"), "tls secret is invaid") - return nil, fmt.Errorf("tls secret is invalid") - } else { - cert, err := tls.X509KeyPair(secret.Data[corev1.TLSCertKey], secret.Data[corev1.TLSPrivateKeyKey]) - if err != nil { - logger.Error(err, "generate X509KeyPair failed") - return nil, err - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(secret.Data["ca.crt"]) - - return &tls.Config{ - InsecureSkipVerify: true, - RootCAs: caCertPool, - Certificates: []tls.Certificate{cert}, - }, nil - } -} - -func (c *RedisFailover) Selector() map[string]string { - if c == nil { - return nil - } - if len(c.replicas) > 0 { - return c.replicas[0].Definition().Spec.Selector.MatchLabels - } - if c.sentinel != nil { - return c.sentinel.Definition().Spec.Selector.MatchLabels - } - return nil -} - -func (c *RedisFailover) IsACLUserExists() bool { - if !c.Version().IsACLSupported() { - return false - } - if len(c.redisUsers) == 0 { - return false - } - for _, v := range c.redisUsers { - if v == nil { - return false - } - } - return true -} diff --git a/pkg/ops/cluster/actor/actor_ensure_slots.go b/pkg/ops/cluster/actor/actor_ensure_slots.go deleted file mode 100644 index 2e0bbc5..0000000 --- a/pkg/ops/cluster/actor/actor_ensure_slots.go +++ /dev/null @@ -1,233 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - "time" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" -) - -var _ actor.Actor = (*actorEnsureSlots)(nil) - -func NewEnsureSlotsActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorEnsureSlots{ - client: client, - logger: logger, - } -} - -type actorEnsureSlots struct { - client kubernetes.ClientSet - - logger logr.Logger -} - -// SupportedCommands -func (a *actorEnsureSlots) SupportedCommands() []actor.Command { - return []actor.Command{cops.CommandEnsureSlots} -} - -// Do -// 该 Actor 处理槽分配与槽丢失的情况, 槽的迁移由 Rebanalce + sidecar 处理 -// 处理逻辑如下: -// 判断是否有 master 节点存在,如果无 master 节点,则 failover -// 槽的分配和迁移只根据 cr.status.shards 中预分配的信息来,如果发现某个槽丢失了,以下逻辑会自动添加回来 -// 特殊情况: -// -// 如果发现某个槽被意外移到了其他节点,该槽不会被移动回来,operator 不会处理这个情况,并保留这个状态。在下一个 Reconcile 中,operator 会在cluster inservice 时,刷新 cr.status.shards 信息 -func (a *actorEnsureSlots) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - cluster := val.(types.RedisClusterInstance) - cr := cluster.Definition() - logger := a.logger.WithName(cops.CommandEnsureSlots.String()).WithValues("namespace", cr.Namespace, "name", cr.Name) - logger.Info("ensure slots") - - // force refresh the cluster - if err := cluster.Refresh(ctx); err != nil { - logger.Error(err, "refresh cluster info failed") - return actor.NewResultWithError(cops.CommandRequeue, err) - } - if len(cluster.Shards()) == 0 { - return actor.NewResult(cops.CommandEnsureResource) - } - - // check is slots fullfilled - var ( - allSlots = slot.NewSlots() - shardsSlots = map[int]types.RedisClusterShard{} - ) - for i, shard := range cluster.Shards() { - if i != shard.Index() { - return actor.NewResult(cops.CommandEnsureResource) - } - allSlots = allSlots.Union(shard.Slots()) - shardsSlots[shard.Index()] = shard - - if shard.Master() != nil { - continue - } - if len(shard.Nodes()) != int(cluster.Definition().Spec.ClusterReplicas)+1 { - return actor.NewResult(cops.CommandRequeue) - } - for _, node := range shard.Nodes() { - if node.Status() == corev1.PodPending || node.IsTerminating() { - return actor.NewResult(cops.CommandHealPod) - } - if !node.IsReady() { - return actor.NewResult(cops.CommandRequeue) - } - } - } - if allSlots.IsFullfilled() { - return nil - } - - var ( - failedShards []types.RedisClusterShard - validMasterCount int - aliveMasterCount int - ) - - // Only masters will slots can vote for the slave to promote to be a master - for _, shard := range cr.Status.Shards { - for _, status := range shard.Slots { - if status.Status == slot.SlotAssigned.String() { - validMasterCount += 1 - break - } - } - } - - // check shard master info - // NOTE: here not check the case when the last statefulset is removed - for i := 0; i < len(cluster.Shards()); i++ { - shard := cluster.Shards()[i] - if shard.Index() != i { - // some shard missing, fix them first. this should not happen here - return actor.NewResult(cops.CommandEnsureResource) - } - // check if master exists - if shard.Master() == nil { - if len(shard.Replicas()) == 0 { - // pod missing - return actor.NewResult(cops.CommandHealPod) - } - failedShards = append(failedShards, shard) - } else { - aliveMasterCount += 1 - } - } - - if len(failedShards) > 0 { - args := []interface{}{"CLUSTER", "FAILOVER"} - for _, shard := range failedShards { - args = args[0:2] - takeover := aliveMasterCount*2 < validMasterCount - if takeover { - args = append(args, "TAKEOVER") - } else { - args = append(args, "FORCE") - } - for _, node := range shard.Replicas() { - if node.IsTerminating() { - continue - } - if subRet := func() *actor.ActorResult { - ctx, cancel := context.WithTimeout(ctx, time.Second*10) - defer cancel() - - logger.Info("do replica failover", "node", node.GetName(), "action", args[2]) - if err := node.Setup(ctx, args); err != nil { - logger.Error(err, "do replica failover failed", "node", node.GetName()) - // slave unexpectedly not reachable, requeue - return actor.NewResult(cops.CommandRequeue) - } - time.Sleep(time.Second * 5) - - if err := node.Refresh(ctx); err != nil { - logger.Error(err, "refresh node info failed, try with other failover") - return nil - } - return nil - }(); subRet != nil { - return subRet - } - - // failover succees - if node.Role() == redis.RedisRoleMaster { - aliveMasterCount += 1 - break - } - } - } - return actor.NewResult(cops.CommandRequeue) - } - - needRefresh := false - for _, shardStatus := range cluster.Definition().Status.Shards { - assignedSlots := slot.NewSlots() - for _, status := range shardStatus.Slots { - _ = assignedSlots.Set(status.Slots, slot.NewSlotAssignStatusFromString(status.Status)) - } - shard := shardsSlots[int(shardStatus.Index)] - node := shard.Master() - if err := node.Refresh(ctx); err != nil { - logger.Error(err, "refresh node failed") - return actor.NewResultWithError(cops.CommandRequeue, err) - } - if node.Role() != redis.RedisRoleMaster { - continue - } - - // NOTE: the operator will update the shards status when the cluster is ok in service - - // if one slots migrated manully, the operator will not move it back - // assign the slot back to current master - missingSlots := assignedSlots.Sub(shard.Slots()).Sub(allSlots).Slots() - if len(missingSlots) > 0 { - if len(missingSlots) < shard.Slots().Count(slot.SlotAssigned) { - logger.Info("WARNING: as if the slots have been moved unexpectedly, this may case the cluster run in an unbalance state") - } - args := []interface{}{"CLUSTER", "ADDSLOTS"} - for _, slot := range missingSlots { - args = append(args, slot) - } - if err := node.Setup(ctx, args); err != nil { - logger.Error(err, "assign slots to shard failed", "shard", shard.GetName()) - return actor.NewResultWithError(cops.CommandRequeue, err) - } - needRefresh = true - } - } - - if needRefresh { - // force refresh the cluster - if err := cluster.Refresh(ctx); err != nil { - logger.Error(err, "refresh cluster info failed") - return actor.NewResultWithError(cops.CommandRequeue, err) - } - } - return nil -} diff --git a/pkg/ops/cluster/actor/actor_heal_pod.go b/pkg/ops/cluster/actor/actor_heal_pod.go deleted file mode 100644 index 51bb378..0000000 --- a/pkg/ops/cluster/actor/actor_heal_pod.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - "time" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/ops/cluster" - cops "github.com/alauda/redis-operator/pkg/ops/cluster" - "github.com/alauda/redis-operator/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/go-logr/logr" -) - -var _ actor.Actor = (*actorHealPod)(nil) - -func NewHealPodActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorHealPod{ - client: client, - logger: logger, - } -} - -type actorHealPod struct { - client kubernetes.ClientSet - logger logr.Logger -} - -func (a *actorHealPod) SupportedCommands() []actor.Command { - return []actor.Command{cluster.CommandHealPod} -} - -// Do -func (a *actorHealPod) Do(ctx context.Context, cluster types.RedisInstance) *actor.ActorResult { - logger := a.logger.WithName(cops.CommandHealPod.String()).WithValues("namespace", cluster.GetNamespace(), "name", cluster.GetName()) - - // clean terminating pods - var ( - now = time.Now() - isUpdated = false - ) - - for _, node := range cluster.Nodes() { - timestamp := node.GetDeletionTimestamp() - if timestamp == nil { - continue - } - grace := time.Second * 30 - if val := node.GetDeletionGracePeriodSeconds(); val != nil { - grace = time.Duration(*val) * time.Second - } - grace += time.Minute * 5 - - if now.Sub(timestamp.Time) <= grace { - continue - } - - objKey := client.ObjectKey{Namespace: node.GetNamespace(), Name: node.GetName()} - logger.V(2).Info("for delete pod", "name", node.GetName()) - // force delete the terminating pods - if err := a.client.DeletePod(ctx, cluster.GetNamespace(), node.GetName(), true); err != nil { - logger.Error(err, "force delete pod failed", "target", objKey) - } else { - isUpdated = true - logger.Info("force delete blocked terminating pod", "target", objKey) - } - } - if isUpdated { - return actor.NewResult(cops.CommandRequeue) - } - if len(cluster.Nodes()) == 0 { - return actor.NewResult(cops.CommandEnsureResource) - } - return nil -} diff --git a/pkg/ops/cluster/command.go b/pkg/ops/cluster/command.go deleted file mode 100644 index 264f801..0000000 --- a/pkg/ops/cluster/command.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cluster - -import "github.com/alauda/redis-operator/pkg/actor" - -type clusterCommand struct { - typ string -} - -func (c *clusterCommand) String() string { - if c == nil { - return "" - } - return c.typ -} - -var ( - CommandUpdateAccount actor.Command = &clusterCommand{typ: "CommandUpdateAccount"} - CommandUpdateConfig = &clusterCommand{typ: "CommandUpdateConfig"} - CommandEnsureResource = &clusterCommand{typ: "CommandEnsureResource"} - CommandHealPod = &clusterCommand{typ: "CommandHealPod"} - CommandCleanResource = &clusterCommand{typ: "CommandCleanResource"} - - CommandJoinNode = &clusterCommand{typ: "CommandJoinNode"} - CommandEnsureSlots = &clusterCommand{typ: "CommandEnsureSlots"} - CommandRebalance = &clusterCommand{typ: "CommandRebalance"} - - CommandRequeue = &clusterCommand{typ: "CommandRequeue"} - CommandAbort = &clusterCommand{typ: "CommandAbort"} - CommandPaused = &clusterCommand{typ: "CommandPaused"} -) diff --git a/pkg/ops/ops.go b/pkg/ops/ops.go deleted file mode 100644 index 5ffc842..0000000 --- a/pkg/ops/ops.go +++ /dev/null @@ -1,330 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package ops - -import ( - "context" - "fmt" - "runtime/debug" - "time" - - v1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/clientset" - clustermodel "github.com/alauda/redis-operator/pkg/models/cluster" - sentinelmodle "github.com/alauda/redis-operator/pkg/models/sentinel" - "github.com/alauda/redis-operator/pkg/ops/cluster" - "github.com/alauda/redis-operator/pkg/ops/sentinel" - "github.com/alauda/redis-operator/pkg/types" - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type ContextKeyType string - -const ( - ContextKeyDepth ContextKeyType = "depth" - ContextKeyLastCommand ContextKeyType = "last_command" -) - -type RuleEngine interface { - Inspect(ctx context.Context, instance types.RedisInstance) *actor.ActorResult -} - -// OpEngine -type OpEngine struct { - client kubernetes.ClientSet - eventRecorder record.EventRecorder - - clusterRuleEngine RuleEngine - sentinelRulEngine RuleEngine - actorManager *actor.ActorManager - - logger logr.Logger -} - -// NewOpEngine -func NewOpEngine(client client.Client, eventRecorder record.EventRecorder, manager *actor.ActorManager, logger logr.Logger) (*OpEngine, error) { - if client == nil { - return nil, fmt.Errorf("require k8s clientset") - } - if eventRecorder == nil { - return nil, fmt.Errorf("require k8s event recorder") - } - if manager == nil { - return nil, fmt.Errorf("require actor manager") - } - - e := OpEngine{ - client: clientset.New(client, logger), - eventRecorder: eventRecorder, - actorManager: manager, - logger: logger.WithName("OpEngine"), - } - - if engine, err := cluster.NewRuleEngine(e.client, eventRecorder, logger); err != nil { - return nil, err - } else { - e.clusterRuleEngine = engine - } - - if engine, err := sentinel.NewRuleEngine(e.client, eventRecorder, logger); err != nil { - return nil, err - } else { - e.sentinelRulEngine = engine - } - return &e, nil -} - -// Run -func (e *OpEngine) Run(ctx context.Context, val interface{}) (ctrl.Result, error) { - // one reconcile only keeps 5minutes - ctx, cancel := context.WithTimeout(ctx, time.Minute*5) - defer cancel() - - switch instance := val.(type) { - case *v1.RedisFailover: - failover, err := e.loadFailoverInstance(ctx, instance) - if err != nil { - return ctrl.Result{}, err - } - return e.reconcile(ctx, failover) - case *v1alpha1.DistributedRedisCluster: - cluster, err := e.loadClusterInstance(ctx, instance) - if err != nil { - return ctrl.Result{}, err - } - return e.reconcile(ctx, cluster) - } - - return ctrl.Result{RequeueAfter: DefaultRequeueDuration}, nil -} - -const ( - MaxCallDepth = 15 - DefaultRequeueDuration = time.Second * 10 - DefaultAbortRequeueDuration = time.Minute - PauseRequeueDuration = time.Minute * 10 - DefaultReconcileTimeout = time.Minute * 5 -) - -func (e *OpEngine) reconcile(ctx context.Context, val types.RedisInstance) (requeue ctrl.Result, err error) { - logger := e.logger.WithValues("namespace", val.GetNamespace(), "name", val.GetName()) - - defer func() { - if e := recover(); e != nil { - logger.Error(fmt.Errorf("%s", e), "reconcile panic") - err = fmt.Errorf("%s", e) - debug.PrintStack() - } - }() - - inspect := func(ctx context.Context, ins types.RedisInstance) *actor.ActorResult { - switch instance := val.(type) { - case types.RedisFailoverInstance: - if instance.Definition() != nil { - logger.Info("inspect sentinel", "ns", instance.Definition().Namespace, "target", instance.Definition().GetName()) - return e.sentinelRulEngine.Inspect(ctx, ins) - } - case types.RedisClusterInstance: - if instance.Definition() != nil { - logger.Info("inspect cluster", "ns", instance.Definition().Namespace, "target", instance.Definition().GetName()) - return e.clusterRuleEngine.Inspect(ctx, ins) - } - - } - return actor.NewResult(cluster.CommandAbort) - } - - actorDo := func(ctx context.Context, a actor.Actor, ins types.RedisInstance) *actor.ActorResult { - return a.Do(ctx, ins) - } - - sendEvent := func(ins types.RedisInstance, typ, reason string, msg string, args ...interface{}) { - switch instance := val.(type) { - case types.RedisFailoverInstance: - if instance.Definition() != nil { - e.eventRecorder.Eventf(instance.Definition(), typ, reason, msg, args...) - } - case types.RedisClusterInstance: - if instance.Definition() != nil { - e.eventRecorder.Eventf(instance.Definition(), typ, reason, msg, args...) - } - } - } - - updateStatus := func(ctx context.Context, ins types.RedisInstance, status v1alpha1.ClusterStatus, stStatus v1.RedisFailoverStatus, msg string) error { - switch instance := val.(type) { - case types.RedisFailoverInstance: - if instance.Definition() != nil { - return instance.UpdateStatus(ctx, stStatus) - } - case types.RedisClusterInstance: - if instance.Definition() != nil { - return instance.UpdateStatus(ctx, status, msg, nil) - } - } - - return nil - } - - var ( - failMsg string - crStatus v1alpha1.ClusterStatus - stStatus v1.RedisFailoverStatus - objKey = client.ObjectKey{Namespace: val.GetNamespace(), Name: val.GetName()} - ret = inspect(ctx, val) - lastCommand actor.Command - requeueDuration time.Duration - ) - if ret == nil { - switch instance := val.(type) { - case types.RedisClusterInstance: - if instance.Definition() != nil { - ret = actor.NewResult(cluster.CommandEnsureResource) - } - case types.RedisFailoverInstance: - if instance.Definition() != nil { - ret = actor.NewResult(sentinel.CommandEnsureResource) - } - } - } - -__end__: - for depth := 0; ret != nil && depth <= MaxCallDepth; depth += 1 { - if depth == MaxCallDepth { - msg := fmt.Sprintf("event loop reached %d threshold, abort current loop", MaxCallDepth) - if lastCommand != nil { - msg = fmt.Sprintf("event loop reached %d threshold, lastCommand %s, abort current loop", MaxCallDepth, lastCommand) - } - sendEvent(val, corev1.EventTypeWarning, "ThresholdLimit", msg) - // use depth to limit the call black hole - logger.Info(fmt.Sprintf("reconcile call depth exceeds %d", MaxCallDepth), "force requeue", "target", objKey) - - requeueDuration = time.Second * 30 - break __end__ - } - msg := fmt.Sprintf("run command %s, depth %d", ret.NextCommand().String(), depth) - if lastCommand != nil { - msg = fmt.Sprintf("run command %s, last command %s, depth %d", ret.NextCommand().String(), lastCommand.String(), depth) - } - logger.Info(msg) - - switch ret.NextCommand() { - case cluster.CommandAbort, sentinel.CommandAbort: - requeueDuration, _ = ret.Result().(time.Duration) - if err := ret.Err(); err != nil { - logger.Error(err, "reconcile aborted", "target", objKey) - failMsg = err.Error() - stStatus.Message = failMsg - crStatus = v1alpha1.ClusterStatusKO - stStatus.Phase = v1.PhaseFail - } else { - logger.Info("reconcile aborted", "target", objKey) - } - if requeueDuration == 0 { - requeueDuration = DefaultAbortRequeueDuration - } - break __end__ - case cluster.CommandRequeue, sentinel.CommandRequeue: - requeueDuration, _ = ret.Result().(time.Duration) - if err := ret.Err(); err != nil { - logger.Error(err, "reconcile requeue", "target", objKey.Name) - failMsg = err.Error() - stStatus.Message = failMsg - } else { - logger.Info("reconcile requeue", "target", objKey.Name) - } - break __end__ - case cluster.CommandPaused, sentinel.CommandPaused: - requeueDuration, _ = ret.Result().(time.Duration) - crStatus = v1alpha1.ClusterStatusPaused - stStatus.Phase = v1.PhasePaused - if err := ret.Err(); err != nil { - logger.Error(err, "reconcile requeue", "target", objKey.Name) - failMsg = err.Error() - stStatus.Message = failMsg - } else { - logger.Info("reconcile requeue", "target", objKey.Name) - } - if requeueDuration == 0 { - requeueDuration = PauseRequeueDuration - } - break __end__ - } - - ctx = context.WithValue(ctx, ContextKeyDepth, depth) - if lastCommand != nil { - ctx = context.WithValue(ctx, ContextKeyLastCommand, lastCommand.String()) - } - - actors := e.actorManager.Get(ret.NextCommand()) - if len(actors) == 0 { - err := fmt.Errorf("unknown command %s", ret.NextCommand()) - logger.Error(err, "actor for command not register") - return ctrl.Result{}, err - } - lastCommand = ret.NextCommand() - - if ret = actorDo(ctx, actors[0], val); ret == nil { - logger.V(3).Info("actor return nil, inspect again") - ret = inspect(ctx, val) - } - - if ret != nil && lastCommand != nil && ret.NextCommand().String() == lastCommand.String() { - ret = actor.NewResult(cluster.CommandRequeue) - } - depth += 1 - } - - if err := updateStatus(ctx, val, crStatus, stStatus, failMsg); err != nil { - if errors.IsNotFound(err) { - return ctrl.Result{}, nil - } - logger.Error(err, "update status before requeue failed") - } - if requeueDuration == 0 { - requeueDuration = DefaultRequeueDuration - } - return ctrl.Result{RequeueAfter: requeueDuration}, nil -} - -// loadClusterInstance -func (e *OpEngine) loadClusterInstance(ctx context.Context, ins *v1alpha1.DistributedRedisCluster) (types.RedisClusterInstance, error) { - if c, err := clustermodel.NewRedisCluster(ctx, e.client, ins, e.logger); err != nil { - e.logger.Error(err, "load cluster failed", "target", client.ObjectKeyFromObject(ins)) - return nil, err - } else { - return c, nil - } -} - -// loadFailoverInstance -func (e *OpEngine) loadFailoverInstance(ctx context.Context, ins *v1.RedisFailover) (types.RedisFailoverInstance, error) { - if c, err := sentinelmodle.NewRedisFailover(ctx, e.client, ins, e.logger); err != nil { - e.logger.Error(err, "load failover failed", "target", client.ObjectKeyFromObject(ins)) - return nil, err - } else { - return c, nil - } -} diff --git a/pkg/ops/sentinel/actor/actor_ensure_resource.go b/pkg/ops/sentinel/actor/actor_ensure_resource.go deleted file mode 100644 index 5000ab2..0000000 --- a/pkg/ops/sentinel/actor/actor_ensure_resource.go +++ /dev/null @@ -1,810 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - "fmt" - "os" - "reflect" - "sort" - "strconv" - "time" - - v1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - redisbackup "github.com/alauda/redis-operator/api/redis/v1" - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/ops/sentinel" - sops "github.com/alauda/redis-operator/pkg/ops/sentinel" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - appv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/utils/strings/slices" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ actor.Actor = (*actorEnsureResource)(nil) - -func NewEnsureResourceActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorEnsureResource{ - client: client, - logger: logger, - } -} - -type actorEnsureResource struct { - client kubernetes.ClientSet - logger logr.Logger -} - -func (a *actorEnsureResource) SupportedCommands() []actor.Command { - return []actor.Command{sentinel.CommandEnsureResource} -} - -// Do -func (a *actorEnsureResource) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - sentinel := val.(types.RedisFailoverInstance) - logger := a.logger.WithName(sops.CommandEnsureResource.String()).WithValues("namespace", sentinel.GetNamespace(), "name", sentinel.GetName()) - - if (sentinel.Definition().Spec.Redis.PodAnnotations != nil) && sentinel.Definition().Spec.Redis.PodAnnotations[config.PAUSE_ANNOTATION_KEY] != "" { - - if ret := a.pauseStatefulSet(ctx, sentinel, logger); ret != nil { - return ret - } - if ret := a.pauseBackupCronJob(ctx, sentinel, logger); ret != nil { - return ret - } - if ret := a.pauseDeployment(ctx, sentinel, logger); ret != nil { - return ret - } - return actor.NewResult(sops.CommandPaused) - } - if ret := a.ensureServiceAccount(ctx, sentinel, logger); ret != nil { - return ret - } - if ret := a.ensureService(ctx, sentinel, logger); ret != nil { - return ret - } - - // ensure extra 资源, - if ret := a.ensureExtraResource(ctx, sentinel, logger); ret != nil { - return ret - } - // ensure secret - // if ret := a.ensureRedisUser(ctx, sentinel, logger); ret != nil { - // return ret - // } - - // ensure configMap - if ret := a.ensureConfigMap(ctx, sentinel, logger); ret != nil { - return ret - } - - // ensure statefulSet - if ret := a.ensureRedisStatefulSet(ctx, sentinel, logger); ret != nil { - return ret - } - - // ensure deployment - - if ret := a.ensureRedisDeployment(ctx, sentinel, logger); ret != nil { - return ret - } - - if ret := a.ensureBackupSchedule(ctx, sentinel, logger); ret != nil { - return ret - } - - return nil -} - -func (a *actorEnsureResource) ensureRedisDeployment(ctx context.Context, sentinel types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := sentinel.Definition() - selector := sentinel.Selector() - if ret := a.ensureDeployPodDisruptionBudget(ctx, cr, logger, selector); ret != nil { - return ret - } - - deploy := sentinelbuilder.GenerateSentinelDeployment(cr, selector) - // get old deployment - oldDeploy, err := a.client.GetDeployment(ctx, cr.Namespace, deploy.Name) - if errors.IsNotFound(err) { - if err := a.client.CreateDeployment(ctx, cr.Namespace, deploy); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - return nil - } else if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if sentinelbuilder.DiffDeployment(deploy, oldDeploy, logger) { - err = a.client.UpdateDeployment(ctx, cr.Namespace, deploy) - if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - return nil -} - -func (a *actorEnsureResource) ensureRedisStatefulSet(ctx context.Context, sentinel types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := sentinel.Definition() - selector := sentinel.Selector() - // ensure Sentinel statefulSet - if ret := a.ensurePodDisruptionBudget(ctx, cr, logger, selector); ret != nil { - return ret - } - isRestoring := func(sts *appv1.StatefulSet) bool { - if sts == nil { - return true - } - if util.GetContainerByName(&sts.Spec.Template.Spec, sentinelbuilder.RestoreContainerName) == nil { - return true - } - return false - } - backupResourceExists := false - var backup *redisbackup.RedisBackup - if cr.Spec.Redis.Restore.BackupName != "" { - var err error - backup, err = a.client.GetRedisBackup(ctx, cr.Namespace, cr.Spec.Redis.Restore.BackupName) - if err == nil { - backupResourceExists = true - } else { - logger.Info("backup resource not found", "backupName", cr.Spec.Redis.Restore.BackupName) - } - } - sts := sentinelbuilder.GenerateRedisStatefulSet(cr, backup, selector, "") - if sentinel.Version().IsACLSupported() { - sts = sentinelbuilder.GenerateRedisStatefulSet(cr, backup, selector, sentinel.Users().GetOpUser().Password.SecretName) - } - err := a.client.CreateIfNotExistsStatefulSet(ctx, cr.Namespace, sts) - if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - - oldSts, err := a.client.GetStatefulSet(ctx, cr.Namespace, sts.Name) - if errors.IsNotFound(err) { - err := a.client.CreateStatefulSet(ctx, cr.Namespace, sts) - if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - return nil - } else if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if !backupResourceExists && isRestoring(sts) { - sts = sentinelbuilder.GenerateRedisStatefulSet(cr, backup, selector, "") - if sentinel.Version().IsACLSupported() { - sts = sentinelbuilder.GenerateRedisStatefulSet(cr, nil, selector, sentinel.Users().GetOpUser().Password.SecretName) - } - } - if !reflect.DeepEqual(oldSts.Spec.Selector.MatchLabels, sts.Spec.Selector.MatchLabels) { - sts.Spec.Selector.MatchLabels = oldSts.Spec.Selector.MatchLabels - } - if clusterbuilder.IsRedisClusterStatefulsetChanged(sts, oldSts, logger) { - if err = a.client.UpdateStatefulSet(ctx, cr.Namespace, sts); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - return nil -} - -func (a *actorEnsureResource) ensureDeployPodDisruptionBudget(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { - pdb := sentinelbuilder.NewDeployPodDisruptionBudgetForCR(rf, selectors) - - if oldPdb, err := a.client.GetPodDisruptionBudget(context.TODO(), rf.Namespace, pdb.Name); errors.IsNotFound(err) { - if err := a.client.CreatePodDisruptionBudget(ctx, rf.Namespace, pdb); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } else if !reflect.DeepEqual(oldPdb.Spec.Selector, pdb.Spec.Selector) { - oldPdb.Labels = pdb.Labels - if err := a.client.UpdatePodDisruptionBudget(ctx, rf.Namespace, oldPdb); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - return nil -} - -func (a *actorEnsureResource) ensurePodDisruptionBudget(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { - - pdb := sentinelbuilder.NewPodDisruptionBudgetForCR(rf, selectors) - - if oldPdb, err := a.client.GetPodDisruptionBudget(context.TODO(), rf.Namespace, pdb.Name); errors.IsNotFound(err) { - if err := a.client.CreatePodDisruptionBudget(ctx, rf.Namespace, pdb); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } else if !reflect.DeepEqual(oldPdb.Spec.Selector, pdb.Spec.Selector) { - oldPdb.Labels = pdb.Labels - oldPdb.Spec.Selector = pdb.Spec.Selector - if err := a.client.UpdatePodDisruptionBudget(ctx, rf.Namespace, oldPdb); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - return nil -} - -/* -func (a *actorEnsureResource) ensureRedisUser(ctx context.Context, sentinel types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - if !sentinel.Version().IsACLSupported() { - return nil - } - cr := sentinel.Definition() - secret := sentinelbuilder.NewSentinelOpSecret(cr) - if err := a.client.CreateIfNotExistsSecret(ctx, cr.Namespace, secret); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - for _, _user := range sentinel.Users() { - if _user.Name == user.DefaultUserName { - ru := sentinelbuilder.GenerateSentinelDefaultRedisUser(sentinel.Definition(), sentinel.Definition().Spec.Auth.SecretPath) - oldRu, err := a.client.GetRedisUser(ctx, sentinel.GetNamespace(), ru.Name) - if err == nil { - oldRu.Spec.PasswordSecrets = ru.Spec.PasswordSecrets - ru = *oldRu - } - if err := a.client.CreateOrUpdateRedisUser(ctx, &ru); err != nil { - a.logger.Error(err, "update default user redisUser failed") - } - } - if _user.Name == user.DefaultOperatorUserName { - ru := sentinelbuilder.GenerateSentinelOperatorsRedisUser(sentinel, _user.Password.SecretName) - - oldRu, err := a.client.GetRedisUser(ctx, sentinel.GetNamespace(), ru.Name) - if err == nil { - oldRu.Spec.PasswordSecrets = ru.Spec.PasswordSecrets - ru = *oldRu - } - if err := a.client.CreateOrUpdateRedisUser(ctx, &ru); err != nil { - a.logger.Error(err, "update operator user redisUser failed") - } - } - } - return nil -} -*/ - -func (a *actorEnsureResource) ensureConfigMap(ctx context.Context, sentinel types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := sentinel.Definition() - selector := sentinel.Selector() - // ensure Sentinel configMap - if ret := a.ensureSentinelConfigMap(ctx, cr, logger, selector); ret != nil { - return ret - } - // ensure Redis configMap - if ret := a.ensureRedisConfigMap(ctx, sentinel, logger, selector); ret != nil { - return ret - } - - // ensure Redis script configMap - if ret := a.ensureRedisScriptConfigMap(ctx, cr, logger, selector); ret != nil { - return ret - } - - if ret := sentinelbuilder.NewSentinelAclConfigMap(cr, sentinel.Users().Encode()); ret != nil { - if err := a.client.CreateIfNotExistsConfigMap(ctx, cr.Namespace, ret); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - return nil -} - -func (a *actorEnsureResource) ensureSentinelConfigMap(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { - - //ensure sentinel config - senitnelConfigMap := sentinelbuilder.NewSentinelConfigMap(rf, selectors) - err := a.client.CreateIfNotExistsConfigMap(ctx, rf.Namespace, senitnelConfigMap) - if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.UpdateIfConfigMapChanged(ctx, senitnelConfigMap); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - - //ensure sentinel probe config - senitnelProbeConfigMap := sentinelbuilder.NewSentinelProbeConfigMap(rf, selectors) - if err = a.client.CreateIfNotExistsConfigMap(ctx, rf.Namespace, senitnelProbeConfigMap); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.UpdateIfConfigMapChanged(ctx, senitnelProbeConfigMap); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - - return nil -} - -func (a *actorEnsureResource) ensureRedisConfigMap(ctx context.Context, st types.RedisFailoverInstance, logger logr.Logger, selectors map[string]string) *actor.ActorResult { - rf := st.Definition() - if ret := a.ensureRedisConfig(ctx, st, logger, selectors); ret != nil { - return ret - } - if ret := a.ensureRedisScriptConfigMap(ctx, rf, logger, selectors); ret != nil { - return ret - } - return nil -} - -func (a *actorEnsureResource) ensureRedisConfig(ctx context.Context, st types.RedisFailoverInstance, logger logr.Logger, selectors map[string]string) *actor.ActorResult { - rf := st.Definition() - configMap := sentinelbuilder.NewRedisConfigMap(st, selectors) - if err := a.client.CreateIfNotExistsConfigMap(ctx, rf.Namespace, configMap); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.UpdateIfConfigMapChanged(ctx, configMap); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - return nil -} - -func (a *actorEnsureResource) ensureRedisScriptConfigMap(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selector map[string]string) *actor.ActorResult { - configMap := sentinelbuilder.NewRedisScriptConfigMap(rf, selector) - if err := a.client.CreateIfNotExistsConfigMap(ctx, rf.Namespace, configMap); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.UpdateIfConfigMapChanged(ctx, configMap); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - return nil -} - -func (a *actorEnsureResource) ensureExtraResource(ctx context.Context, sentinel types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := sentinel.Definition() - selectors := sentinel.Selector() - // ensure Redis ssl - if cr.Spec.EnableTLS { - if ret := a.ensureRedisSSL(ctx, cr, logger, selectors); ret != nil { - return ret - } - } - if ret := a.ensureServiceMonitor(ctx, cr, logger); ret != nil { - return ret - } - - return nil -} - -func (a *actorEnsureResource) ensureServiceMonitor(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger) *actor.ActorResult { - if !rf.Spec.Redis.Exporter.Enabled { - return nil - } - watchNamespace := os.Getenv("WATCH_NAMESPACE") - if watchNamespace == "" { - watchNamespace = "operators" - } - sentinelLabels := map[string]string{ - "app.kubernetes.io/part-of": "redis-failover", - } - sentinelSM := sentinelbuilder.NewServiceMonitorForCR(rf, sentinelLabels) - sentinelSM.Namespace = watchNamespace - if oldsm, err := a.client.GetServiceMonitor(ctx, watchNamespace, sentinelSM.Name); err != nil { - if errors.IsNotFound(err) { - if err := a.client.CreateServiceMonitor(context.TODO(), watchNamespace, sentinelSM); err != nil { - logger.Info(err.Error()) - } - } - logger.Info(err.Error()) - } else { - if len(oldsm.OwnerReferences) == 0 { - if err := a.client.CreateOrUpdateServiceMonitor(context.TODO(), watchNamespace, sentinelSM); err != nil { - logger.Info(err.Error()) - } - } - if len(oldsm.Spec.Endpoints) == 0 { - if err := a.client.CreateOrUpdateServiceMonitor(context.TODO(), watchNamespace, sentinelSM); err != nil { - logger.Info(err.Error()) - } - } else { - if len(oldsm.Spec.Endpoints[0].MetricRelabelConfigs) == 0 { - if err := a.client.CreateOrUpdateServiceMonitor(context.TODO(), watchNamespace, sentinelSM); err != nil { - logger.Info(err.Error()) - } - } - if rf.Spec.ServiceMonitor.CustomMetricRelabelings { - if err := a.client.CreateOrUpdateServiceMonitor(context.TODO(), watchNamespace, sentinelSM); err != nil { - logger.Info(err.Error()) - } - } - } - } - return nil -} - -func (a *actorEnsureResource) ensureRedisSSL(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { - cert := sentinelbuilder.NewCertificate(rf, selectors) - if err := a.client.CreateIfNotExistsCertificate(ctx, rf.Namespace, cert); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - oldCert, err := a.client.GetCertificate(ctx, rf.Namespace, cert.GetName()) - if err != nil && !errors.IsNotFound(err) { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - var ( - found = false - secretName = sentinelbuilder.GetRedisSSLSecretName(rf.Name) - ) - for i := 0; i < 5; i++ { - time.Sleep(time.Second * time.Duration(i)) - - if secret, _ := a.client.GetSecret(ctx, rf.Namespace, secretName); secret != nil { - found = true - break - } - - // check when the certificate created - if time.Since(oldCert.GetCreationTimestamp().Time) > time.Minute*5 { - return actor.NewResultWithError(sops.CommandAbort, fmt.Errorf("issue for tls certificate failed, please check the cert-manager")) - } - } - if !found { - return actor.NewResult(sops.CommandRequeue) - } - - return nil -} - -func (a *actorEnsureResource) pauseDeployment(ctx context.Context, st types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := st.Definition() - name := sentinelbuilder.GetSentinelDeploymentName(cr.Name) - if deploy, err := a.client.GetDeployment(ctx, cr.Namespace, name); err != nil { - if errors.IsNotFound(err) { - return nil - } - return actor.NewResultWithError(sops.CommandRequeue, err) - } else { - if deploy.Spec.Replicas == nil || *deploy.Spec.Replicas == 0 { - return nil - } - *deploy.Spec.Replicas = 0 - if err = a.client.UpdateDeployment(ctx, cr.Namespace, deploy); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - return nil -} - -func (a *actorEnsureResource) pauseStatefulSet(ctx context.Context, st types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := st.Definition() - name := sentinelbuilder.GetSentinelStatefulSetName(cr.Name) - if sts, err := a.client.GetStatefulSet(ctx, cr.Namespace, name); err != nil { - if errors.IsNotFound(err) { - return nil - } - return actor.NewResultWithError(sops.CommandRequeue, err) - } else { - if sts.Spec.Replicas == nil || *sts.Spec.Replicas == 0 { - return nil - } - *sts.Spec.Replicas = 0 - if err = a.client.UpdateStatefulSet(ctx, cr.Namespace, sts); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - return nil -} - -func (a *actorEnsureResource) pauseBackupCronJob(ctx context.Context, st types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := st.Definition() - selectorLabels := map[string]string{ - "redisfailovers.databases.spotahome.com/name": cr.Name, - } - jobsRes, err := a.client.ListCronJobs(ctx, cr.GetNamespace(), client.ListOptions{ - LabelSelector: labels.SelectorFromSet(selectorLabels), - }) - if err != nil { - logger.Error(err, "load cronjobs failed") - return actor.NewResultWithError(sops.CommandRequeue, err) - } - for _, val := range jobsRes.Items { - if err := a.client.DeleteCronJob(ctx, cr.GetNamespace(), val.Name); err != nil { - logger.Error(err, "delete cronjob failed", "target", client.ObjectKeyFromObject(&val)) - } - } - return nil -} - -func (a *actorEnsureResource) ensureServiceAccount(ctx context.Context, sentinel types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := sentinel.Definition() - sa := clusterbuilder.NewServiceAccount(cr) - role := clusterbuilder.NewRole(cr) - binding := clusterbuilder.NewRoleBinding(cr) - clusterRole := clusterbuilder.NewClusterRole(cr) - clusterRoleBinding := clusterbuilder.NewClusterRoleBinding(cr) - - if err := a.client.CreateOrUpdateServiceAccount(ctx, sentinel.GetNamespace(), sa); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.CreateOrUpdateRole(ctx, sentinel.GetNamespace(), role); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.CreateOrUpdateRoleBinding(ctx, sentinel.GetNamespace(), binding); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.CreateOrUpdateClusterRole(ctx, clusterRole); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if oldClusterRb, err := a.client.GetClusterRoleBinding(ctx, clusterRoleBinding.Name); err != nil { - if errors.IsNotFound(err) { - if err := a.client.CreateClusterRoleBinding(ctx, clusterRoleBinding); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else { - exists := false - for _, sub := range oldClusterRb.Subjects { - if sub.Namespace == sentinel.GetNamespace() { - exists = true - } - } - if !exists && len(oldClusterRb.Subjects) > 0 { - oldClusterRb.Subjects = append(oldClusterRb.Subjects, - rbacv1.Subject{Kind: "ServiceAccount", - Name: util.RedisBackupServiceAccountName, - Namespace: sentinel.GetNamespace()}, - ) - err := a.client.CreateOrUpdateClusterRoleBinding(ctx, oldClusterRb) - if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - } - return nil -} - -func (a *actorEnsureResource) ensureService(ctx context.Context, sentinel types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := sentinel.Definition() - //read write svc - rwSvc := sentinelbuilder.NewRWSvcForCR(cr) - roSvc := sentinelbuilder.NewReadOnlyForCR(cr) - if err := a.client.CreateIfNotExistsService(ctx, sentinel.GetNamespace(), rwSvc); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.UpdateIfSelectorChangedService(ctx, sentinel.GetNamespace(), rwSvc); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.CreateIfNotExistsService(ctx, sentinel.GetNamespace(), roSvc); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - if err := a.client.UpdateIfSelectorChangedService(ctx, sentinel.GetNamespace(), roSvc); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - - selector := sentinel.Selector() - senService := sentinelbuilder.NewSentinelServiceForCR(cr, selector) - oldService, err := a.client.GetService(ctx, sentinel.GetNamespace(), senService.Name) - if err != nil { - if errors.IsNotFound(err) { - if err := a.client.CreateIfNotExistsService(ctx, sentinel.GetNamespace(), senService); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else { - // Update the old service or custom port service - if senService.Spec.Ports[0].NodePort != oldService.Spec.Ports[0].NodePort || !reflect.DeepEqual(senService.Spec.Selector, oldService.Spec.Selector) { - if err := a.client.UpdateService(ctx, sentinel.GetNamespace(), senService); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - if err := a.client.UpdateIfSelectorChangedService(ctx, sentinel.GetNamespace(), senService); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - - exporterService := sentinelbuilder.NewExporterServiceForCR(cr, selector) - if err := a.client.CreateIfNotExistsService(ctx, sentinel.GetNamespace(), exporterService); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - - if err := a.client.UpdateIfSelectorChangedService(ctx, sentinel.GetNamespace(), exporterService); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - - if ret := a.ensureRedisNodePortService(ctx, cr, logger, selector); ret != nil { - return ret - } - return nil -} - -func (a *actorEnsureResource) ensureRedisNodePortService(ctx context.Context, rf *v1.RedisFailover, logger logr.Logger, selectors map[string]string) *actor.ActorResult { - if len(rf.Spec.Expose.DataStorageNodePortMap) == 0 && rf.Spec.Expose.DataStorageNodePortSequence == "" { - return nil - } - if !rf.Spec.Expose.EnableNodePort { - return nil - } - - if len(rf.Spec.Expose.DataStorageNodePortMap) > 0 { - logger.Info("EnsureRedisNodePortService DataStorageNodePortMap", "Namepspace", rf.Namespace, "Name", rf.Name) - need_svc_list := []string{} - for index, port := range rf.Spec.Expose.DataStorageNodePortMap { - svc := sentinelbuilder.NewRedisNodePortService(rf, index, port, selectors) - - need_svc_list = append(need_svc_list, svc.Name) - old_svc, err := a.client.GetService(ctx, rf.Namespace, svc.Name) - if err != nil { - if errors.IsNotFound(err) { - _err := a.client.CreateIfNotExistsService(ctx, rf.Namespace, svc) - if _err != nil { - return actor.NewResultWithError(sops.CommandRequeue, _err) - } else { - continue - } - } else { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - if old_svc.Spec.Ports[0].NodePort != port { - old_svc.Spec.Ports[0].NodePort = port - if len(old_svc.OwnerReferences) > 0 && old_svc.OwnerReferences[0].Kind == "Pod" { - if err := a.client.DeletePod(ctx, rf.Namespace, old_svc.Name); err != nil { - logger.Error(err, "DeletePod") - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else { - if err := a.client.CreateOrUpdateService(ctx, rf.Namespace, old_svc); err != nil { - if !errors.IsNotFound(err) { - logger.Error(err, "CreateOrUpdateService") - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - } - } - if !reflect.DeepEqual(old_svc.Labels, svc.Labels) { - old_svc.Labels = svc.Labels - old_svc.Spec.Selector = svc.Spec.Selector - if err := a.client.UpdateService(ctx, rf.Namespace, old_svc); err != nil { - logger.Error(err, "Update servcie labels failed") - } - } - } - //回收多余端口 - svcs, err := a.client.GetServiceByLabels(ctx, rf.Namespace, selectors) - if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - for _, svc := range svcs.Items { - if svc.Spec.Selector["statefulset.kubernetes.io/pod-name"] != "" { - if !slices.Contains(need_svc_list, svc.Name) { - if _, err := a.client.GetPod(ctx, rf.Namespace, svc.Spec.Selector["statefulset.kubernetes.io/pod-name"]); errors.IsNotFound(err) { - if err := a.client.DeleteService(ctx, rf.Namespace, svc.Name); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - } - } - } - } - - svcs, err := a.client.GetServiceByLabels(ctx, rf.Namespace, selectors) - if err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - - for _, svc := range svcs.Items { - if svc.Spec.Selector["statefulset.kubernetes.io/pod-name"] != "" { - pod, err := a.client.GetPod(ctx, rf.Namespace, svc.Spec.Selector["statefulset.kubernetes.io/pod-name"]) - if err == nil { - if pod.Labels["middleware.alauda.io/announce_port"] != "" && - pod.Labels["middleware.alauda.io/announce_port"] != strconv.Itoa(int(svc.Spec.Ports[0].NodePort)) { - _err := a.client.DeletePod(ctx, rf.Namespace, pod.Name) - if _err != nil { - return actor.NewResultWithError(sops.CommandRequeue, _err) - } - } - - } - } - } - return nil -} - -func (a *actorEnsureResource) ensureBackupSchedule(ctx context.Context, st types.RedisFailoverInstance, logger logr.Logger) *actor.ActorResult { - cr := st.Definition() - selectors := st.Selector() - - for _, schedule := range cr.Spec.Redis.Backup.Schedule { - ls := map[string]string{ - "redisfailovers.databases.spotahome.com/name": cr.Name, - "redisfailovers.databases.spotahome.com/scheduleName": schedule.Name, - } - - res, err := a.client.ListRedisBackups(ctx, cr.GetNamespace(), client.ListOptions{ - LabelSelector: labels.SelectorFromSet(ls), - }) - if err != nil { - logger.Error(err, "load backups failed", "target", client.ObjectKeyFromObject(cr)) - return actor.NewResultWithError(sops.CommandRequeue, err) - } - sort.SliceStable(res.Items, func(i, j int) bool { - return res.Items[i].GetCreationTimestamp().After(res.Items[j].GetCreationTimestamp().Time) - }) - for i := len(res.Items) - 1; i >= int(schedule.Keep); i-- { - item := res.Items[i] - if err := a.client.DeleteRedisBackup(ctx, item.GetNamespace(), item.GetName()); err != nil { - logger.V(2).Error(err, "clean old backup failed", "target", client.ObjectKeyFromObject(&item)) - } - } - } - - // check backup schedule - // var desiredCronJob []string - // for _, b := range cr.Spec.Redis.Backup.Schedule { - // desiredCronJob = append(desiredCronJob, clusterbuilder.GenerateCronJobName(cr.Name, b.Name)) - // } - - selectorLabels := map[string]string{ - "redisfailovers.databases.spotahome.com/name": cr.Name, - } - jobsRes, err := a.client.ListCronJobs(ctx, cr.GetNamespace(), client.ListOptions{ - LabelSelector: labels.SelectorFromSet(selectorLabels), - }) - if err != nil { - logger.Error(err, "load cronjobs failed") - return actor.NewResultWithError(sops.CommandRequeue, err) - } - jobsMap := map[string]*batchv1.CronJob{} - for _, item := range jobsRes.Items { - jobsMap[item.Name] = &item - } - for _, sched := range cr.Spec.Redis.Backup.Schedule { - sc := v1alpha1.Schedule{ - Name: sched.Name, - Schedule: sched.Schedule, - Keep: sched.Keep, - KeepAfterDeletion: sched.KeepAfterDeletion, - Storage: v1alpha1.RedisBackupStorage(sched.Storage), - Target: sched.Target, - } - job := sentinelbuilder.NewRedisFailoverBackupCronJobFromCR(sc, cr, selectors) - if oldJob := jobsMap[job.GetName()]; oldJob != nil { - if clusterbuilder.IsCronJobChanged(job, oldJob, logger) { - if err := a.client.UpdateCronJob(ctx, oldJob.GetNamespace(), job); err != nil { - logger.Error(err, "update cronjob failed", "target", client.ObjectKeyFromObject(oldJob)) - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - jobsMap[job.GetName()] = nil - } else if err := a.client.CreateCronJob(ctx, cr.GetNamespace(), job); err != nil { - logger.Error(err, "create redisbackup cronjob failed", "target", client.ObjectKeyFromObject(job)) - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - - for name, val := range jobsMap { - if val == nil { - continue - } - if err := a.client.DeleteCronJob(ctx, cr.GetNamespace(), name); err != nil { - logger.Error(err, "delete cronjob failed", "target", client.ObjectKeyFromObject(val)) - } - } - return nil -} diff --git a/pkg/ops/sentinel/actor/actor_heal_master.go b/pkg/ops/sentinel/actor/actor_heal_master.go deleted file mode 100644 index d90a60a..0000000 --- a/pkg/ops/sentinel/actor/actor_heal_master.go +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - "strconv" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/ops/sentinel" - "github.com/alauda/redis-operator/pkg/redis" - "github.com/alauda/redis-operator/pkg/types" - "github.com/go-logr/logr" -) - -var _ actor.Actor = (*actorHealMaster)(nil) - -func NewActorHealMasterActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorHealMaster{ - client: client, - logger: logger, - } -} - -type actorHealMaster struct { - client kubernetes.ClientSet - logger logr.Logger -} - -func (a *actorHealMaster) SupportedCommands() []actor.Command { - return []actor.Command{sentinel.CommandHealMaster} -} - -func (a *actorHealMaster) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - a.logger.Info("heal master", "sentinel", val.GetName()) - st := val.(types.RedisFailoverInstance) - if len(st.Nodes()) != int(st.Definition().Spec.Redis.Replicas) { - return actor.NewResult(sentinel.CommandEnsureResource) - } - rf := st.Definition() - masterCount := map[redis.Address]int{} - for _, node := range st.SentinelNodes() { - if node.Info().SentinelMaster0.Status == "ok" { - masterCount[node.Info().SentinelMaster0.Address]++ - } - - } - - //获取masterCount 最大的Address - var maxAddress redis.Address - var maxCount int - for address, count := range masterCount { - if count > maxCount { - maxCount = count - maxAddress = address - } - } - if maxCount == 0 { - a.logger.Info("no master found") - } - exists := false - for _, n := range st.Nodes() { - if n.DefaultIP().String() == maxAddress.Host() && n.Port() == maxAddress.Port() { - exists = true - } - } - // 如果 maxCount >= (rf.Spec.Redis.Replicas/2),就将所有redis节点的master设置为masterCount最多的master - if maxCount >= int(rf.Spec.Redis.Replicas/2) { - for _, node := range st.Nodes() { - a.logger.Info("set sentinel's master", "masterIp", maxAddress.Host(), "masterPort", maxAddress.Port(), "exists", exists) - if exists { - if err := node.ReplicaOf(ctx, maxAddress.Host(), strconv.Itoa(maxAddress.Port())); err != nil { - a.logger.Error(err, "set master failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - - } - } else { - for _, node := range st.Nodes() { - if len(st.Masters()) == 1 { - master := st.Masters()[0] - a.logger.Info("set node master", "masterIp", master.DefaultIP(), "masterPort", master.Port()) - if err := node.ReplicaOf(ctx, master.DefaultIP().String(), strconv.Itoa(master.Port())); err != nil { - a.logger.Error(err, "set master failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - } - - return actor.NewResult(sentinel.CommandPatchLabels) -} diff --git a/pkg/ops/sentinel/actor/actor_init_master.go b/pkg/ops/sentinel/actor/actor_init_master.go deleted file mode 100644 index da2cdf8..0000000 --- a/pkg/ops/sentinel/actor/actor_init_master.go +++ /dev/null @@ -1,132 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - "strconv" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/ops/sentinel" - sops "github.com/alauda/redis-operator/pkg/ops/sentinel" - "github.com/alauda/redis-operator/pkg/types" - r_types "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/go-logr/logr" -) - -var _ actor.Actor = (*actorInitMaster)(nil) - -func NewActorInitMasterActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorInitMaster{ - client: client, - logger: logger, - } -} - -type actorInitMaster struct { - client kubernetes.ClientSet - logger logr.Logger -} - -func (a *actorInitMaster) SupportedCommands() []actor.Command { - return []actor.Command{sentinel.CommandInitMaster} -} - -func (a *actorInitMaster) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - - logger := a.logger.WithName("Do").WithValues("namespace", val.GetNamespace(), "name", val.GetName()) - st := val.(types.RedisFailoverInstance) - logger.Info("init master", "sentinel", st.GetName()) - var offsetMap = make(map[r_types.RedisNode]int64) - var createTimestampMap = make(map[r_types.RedisNode]int64) - for _, node := range st.Nodes() { - offsetMap[node] = node.Info().MasterReplOffset - createTimestampMap[node] = node.GetPod().GetCreationTimestamp().Unix() - } - // 如果有master且有slave,则不需要设置master - for _, node := range st.Nodes() { - if node.Info().Role == "master" && node.Info().ConnectedReplicas != 0 { - return nil - } - } - // 获取offset node ,如果max offset >0,且 并设置为master - - var master r_types.RedisNode - var maxOffset int64 = 0 - for _, node := range st.Nodes() { - if offsetMap[node] > maxOffset { - maxOffset = offsetMap[node] - master = node - } - } - - //如果任何一个sentinel的master status为ok,则不需要设置master,等待起自愈 - for _, v := range st.SentinelNodes() { - if v.Info().SentinelMaster0.Status == "ok" { - return nil - } - } - - if maxOffset > 0 { - for _, node := range st.Nodes() { - - if node == master { - logger.Info("(offset) set master", "master", master.GetName()) - if err := node.ReplicaOf(ctx, "no", "one"); err != nil { - logger.Error(err, "set master failed") - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else { - logger.Info("(offset) set slave", "slave", node.GetName(), "master", master.GetName()) - if err := node.ReplicaOf(ctx, master.DefaultIP().String(), strconv.Itoa(master.Port())); err != nil { - logger.Error(err, "set slave failed") - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - } - return nil - } - - // 获取创建时间最早的node,并设置为master - var minTimestamp int64 = 0 - for _, node := range st.Nodes() { - if minTimestamp == 0 { - minTimestamp = createTimestampMap[node] - master = node - } - if createTimestampMap[node] < minTimestamp { - minTimestamp = createTimestampMap[node] - master = node - } - } - for _, node := range st.Nodes() { - if node == master { - logger.Info("(createTime)set master", "master", master.GetName()) - if err := node.ReplicaOf(ctx, "no", "one"); err != nil { - logger.Error(err, "set master failed") - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } else { - logger.Info("(createTime)set slave", "slave", node.GetName(), "master", master.GetName()) - if err := node.ReplicaOf(ctx, master.DefaultIP().String(), strconv.Itoa(master.Port())); err != nil { - return actor.NewResultWithError(sops.CommandRequeue, err) - } - } - } - return actor.NewResult(sops.CommandPatchLabels) -} diff --git a/pkg/ops/sentinel/actor/actor_patch_labels.go b/pkg/ops/sentinel/actor/actor_patch_labels.go deleted file mode 100644 index 8b462a2..0000000 --- a/pkg/ops/sentinel/actor/actor_patch_labels.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/ops/sentinel" - "github.com/alauda/redis-operator/pkg/types" - t_redis "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/go-logr/logr" -) - -var _ actor.Actor = (*actorPatchLabels)(nil) - -func NewPatchLabelsActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorPatchLabels{ - client: client, - logger: logger, - } -} - -type actorPatchLabels struct { - client kubernetes.ClientSet - logger logr.Logger -} - -func (a *actorPatchLabels) SupportedCommands() []actor.Command { - return []actor.Command{sentinel.CommandPatchLabels} -} - -func (a *actorPatchLabels) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - // 给pod添加对应角色的labels - st := val.(types.RedisFailoverInstance) - for _, v := range st.Nodes() { - if v.Definition() != nil { - if v.Role() == t_redis.RedisRoleMaster { - - if v.Definition().GetLabels()[sentinelbuilder.RedisRoleLabel] != sentinelbuilder.RedisRoleMaster { - err := a.client.PatchPodLabel(ctx, v.Definition(), sentinelbuilder.RedisRoleLabel, sentinelbuilder.RedisRoleMaster) - if err != nil { - a.logger.Error(err, "patch pod label failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - - } - if v.Role() == t_redis.RedisRoleSlave { - if v.Definition().GetLabels()[sentinelbuilder.RedisRoleLabel] != sentinelbuilder.RedisRoleSlave { - err := a.client.PatchPodLabel(ctx, v.Definition(), sentinelbuilder.RedisRoleLabel, sentinelbuilder.RedisRoleSlave) - if err != nil { - a.logger.Error(err, "patch pod label failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - } - } - return actor.NewResult(sentinel.CommandSentinelHeal) -} diff --git a/pkg/ops/sentinel/actor/actor_sentinel_heal.go b/pkg/ops/sentinel/actor/actor_sentinel_heal.go deleted file mode 100644 index c2707c2..0000000 --- a/pkg/ops/sentinel/actor/actor_sentinel_heal.go +++ /dev/null @@ -1,198 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - "strconv" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/ops/sentinel" - p_redis "github.com/alauda/redis-operator/pkg/redis" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/go-logr/logr" -) - -var _ actor.Actor = (*actorSentinelHeal)(nil) - -func NewSentinelHealActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorSentinelHeal{ - client: client, - logger: logger, - } -} - -type actorSentinelHeal struct { - client kubernetes.ClientSet - logger logr.Logger -} - -func (a *actorSentinelHeal) SupportedCommands() []actor.Command { - return []actor.Command{sentinel.CommandSentinelHeal} -} - -func (a *actorSentinelHeal) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - st := val.(types.RedisFailoverInstance) - a.logger.Info("sentinel heal", "sentinel", st.GetName()) - user := "" - password := st.Users().GetDefaultUser().Password.String() - if st.Version().IsACLSupported() && st.Users().GetOpUser() != nil { - user = st.Users().GetOpUser().Name - password = st.Users().GetOpUser().Password.String() - } - var replicateMaster redis.RedisNode - if st.Sentinel() != nil && *st.Sentinel().Definition().Spec.Replicas != st.Definition().Spec.Sentinel.Replicas { - return actor.NewResult(sentinel.CommandEnsureResource) - } - // 查看sentinel info中status正常的sentinel - if len(st.Masters()) == 0 { - a.logger.V(3).Info("no master found") - return actor.NewResult(sentinel.CommandEnsureResource) - } else { - replicateMaster = st.Masters()[0] - } - sentinelMasterMap := map[p_redis.Address]int{} - odown := 0 - sdown := 0 - for _, sen := range st.SentinelNodes() { - if sen.Info().SentinelMaster0.Status == "ok" { - sentinelMasterMap[sen.Info().SentinelMaster0.Address] += 1 - } - if sen.Info().SentinelMaster0.Status == "odown" { - odown += 1 - } - if sen.Info().SentinelMaster0.Status == "sdown" { - sdown += 1 - } - //有多的节点是未知节点 - if (sen.Info().SentinelMaster0.Sentinels > int(st.Definition().Spec.Sentinel.Replicas)) || - (sen.Info().SentinelMaster0.Replicas > int(st.Definition().Spec.Redis.Replicas-1)) { - a.logger.Info("sentinel has unknown node") - if sen.CurrentVersion().IsACLSupported() { - if err := sen.Setup(ctx, []interface{}{"sentinel", "set", "mymaster", "auth-user", user}); err != nil { - a.logger.Info("set auth-user failed", "err", err.Error()) - } - if err := sen.Setup(ctx, []interface{}{"sentinel", "set", "mymaster", "auth-password", password}); err != nil { - a.logger.Info("set auth-user failed", "err", err.Error()) - } - } - if err := sen.Setup(ctx, []interface{}{"sentinel", "reset", "*"}); err != nil { - a.logger.Info("reset sen failed", "err", err.Error()) - } - - if err := sen.Refresh(ctx); err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - - //如果所有的节点认为客观下线,刷新所有节点 - if len(st.SentinelNodes()) == odown { - for _, sen := range st.SentinelNodes() { - if err := sen.Setup(ctx, []interface{}{"sentinel", "reset", "*"}); err != nil { - a.logger.Info("odown reset sen failed", "err", err.Error()) - } - } - } - //刷新 主观下线的节点 - if odown == 0 && sdown > 0 && len(st.Masters()) > 0 { - for _, sen := range st.SentinelNodes() { - if sen.Info().SentinelMaster0.Status == "sdown" { - if err := sen.Setup(ctx, []interface{}{"sentinel", "reset", "*"}); err != nil { - a.logger.Info("odown reset sen failed", "err", err.Error()) - } - } - } - } - - a.logger.V(3).Info("sentinelMasterMap", "sentinelMasterMap", sentinelMasterMap) - // 获取sentinelMasterMap 最多count的master address - var maxAddress p_redis.Address - var maxCount int - for address, count := range sentinelMasterMap { - if count > maxCount { - maxCount = count - maxAddress = address - } - } - a.logger.V(3).Info("maxAddress", "maxAddress", maxAddress, "maxCount", maxCount) - quorum := strconv.Itoa(int(st.Definition().Spec.Sentinel.Replicas/2 + 1)) - if replicateMaster.IsReady() { //master ready - if maxCount < 1 { - //redis 有master ,哨兵没有 master - a.logger.Info("redis has master, sentinel has no master") - for _, sen := range st.SentinelNodes() { - //reset mymaster - err := sen.SetMonitor(ctx, replicateMaster.DefaultIP().String(), strconv.Itoa(replicateMaster.Port()), user, password, quorum) - if err != nil { - a.logger.Error(err, "sentinel set monitor failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - - } - } else { - // redis就绪,所有哨兵指向同一master - for _, sen := range st.SentinelNodes() { - if sen.Info().SentinelMaster0.Address != maxAddress { - a.logger.Info("redis ready, all sentinel point to same master") - err := sen.SetMonitor(ctx, maxAddress.Host(), strconv.Itoa(maxAddress.Port()), user, password, quorum) - if err != nil { - a.logger.Error(err, "sentinel set monitor failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - } - } - err := st.Refresh(ctx) - if err != nil { - a.logger.Error(err, "refresh sentinel failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - // 节点在statefulset 中,sentinel 指向最多的master - masterInSts := false - for _, node := range st.Nodes() { - // maxAddress 在 node 中 - if node.DefaultIP().String() == maxAddress.Host() && node.Port() == maxAddress.Port() { - masterInSts = true - for _, sn := range st.SentinelNodes() { - if sn.Info().SentinelMaster0.Address != maxAddress { - a.logger.Info("sts has master, all sentinel point to same master") - err := sn.SetMonitor(ctx, maxAddress.Host(), strconv.Itoa(maxAddress.Port()), user, password, quorum) - if err != nil { - a.logger.Error(err, "sentinel set monitor failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - } - } - if !masterInSts { - a.logger.Info("master not in sts, sentinel set monitor to master") - for _, sn := range st.SentinelNodes() { - err := sn.SetMonitor(ctx, replicateMaster.DefaultIP().String(), strconv.Itoa(replicateMaster.Port()), user, password, quorum) - if err != nil { - a.logger.Error(err, "sentinel set monitor failed") - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - - return nil -} diff --git a/pkg/ops/sentinel/actor/actor_update_account.go b/pkg/ops/sentinel/actor/actor_update_account.go deleted file mode 100644 index 16a21f1..0000000 --- a/pkg/ops/sentinel/actor/actor_update_account.go +++ /dev/null @@ -1,349 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package actor - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/ops/sentinel" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/utils/strings/slices" -) - -var _ actor.Actor = (*actorUpdateAccount)(nil) - -func NewUpdateAccountActor(client kubernetes.ClientSet, logger logr.Logger) actor.Actor { - return &actorUpdateAccount{ - client: client, - logger: logger, - } -} - -type actorUpdateAccount struct { - client kubernetes.ClientSet - - logger logr.Logger -} - -// SupportedCommands -func (a *actorUpdateAccount) SupportedCommands() []actor.Command { - return []actor.Command{sentinel.CommandUpdateAccount} -} - -func (a *actorUpdateAccount) Do(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - sen := val.(types.RedisFailoverInstance) - var ( - isACLAppliedInPods = true - isAllACLSupported = true - isAllPodReady = true - ) - for _, node := range sen.Nodes() { - if !node.CurrentVersion().IsACLSupported() { - isAllACLSupported = false - break - } - // check if acl have been applied to container - if !node.IsACLApplied() { - isACLAppliedInPods = false - } - if node.ContainerStatus() == nil || !node.ContainerStatus().Ready || - node.IsTerminating() { - isAllPodReady = false - } - } - a.logger.V(3).Info("update account", - "isAllACLSupported", isAllACLSupported, - "isACLAppliedInPods", isACLAppliedInPods, - "version", sen.Users().Encode(), - ) - - if isAllACLSupported && isACLAppliedInPods { - opSecretName := sentinelbuilder.GenerateSentinelACLOperatorSecretName(sen.GetName()) - secret := sentinelbuilder.NewSentinelOpSecret(sen.Definition()) - if err := a.client.CreateIfNotExistsSecret(ctx, sen.GetNamespace(), secret); err != nil { - a.logger.Info("create operator secret", "secret", secret) - } - users := sen.Users() - if _, ok := users.Encode()[user.DefaultOperatorUserName]; !ok { - opUser, err := user.NewOperatorUser(secret, sen.Version().IsACL2Supported()) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - users = append(users, opUser) - data := users.Encode() - allApplied := true - if isAllPodReady { - for _, node := range sen.Nodes() { - if err := node.Setup(ctx, formatACLSetCommand(users.GetOpUser())); err != nil { - allApplied = false - a.logger.Error(err, "update acl config failed") - } - } - if allApplied { - err = a.client.CreateOrUpdateConfigMap(ctx, sen.GetNamespace(), sentinelbuilder.NewSentinelAclConfigMap(sen.Definition(), data)) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - return actor.NewResult(sentinel.CommandRequeue) - } else if users.GetOpUser() != nil && sen.Version().IsACL2Supported() { - if len(users.GetOpUser().Rules[0].Channels) == 0 { - users.GetOpUser().Rules[0].Channels = []string{"*"} - } - data := users.Encode() - allApplied := true - if isAllPodReady { - for _, node := range sen.Nodes() { - if err := node.Setup(ctx, formatACLSetCommand(users.GetOpUser())); err != nil { - allApplied = false - a.logger.Error(err, "update acl config failed") - } - } - if allApplied { - err := a.client.CreateOrUpdateConfigMap(ctx, sen.GetNamespace(), sentinelbuilder.NewSentinelAclConfigMap(sen.Definition(), data)) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } - } - - data := users.Encode() - err := a.client.CreateIfNotExistsConfigMap(ctx, sen.GetNamespace(), sentinelbuilder.NewSentinelAclConfigMap(sen.Definition(), data)) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - if sen.Users().GetOpUser() == nil && sen.Version().IsACLSupported() { - ru := sentinelbuilder.GenerateSentinelOperatorsRedisUser(sen, opSecretName) - if err := a.client.CreateIfNotExistsRedisUser(ctx, &ru); err != nil { - a.logger.Error(err, "create operator user redisUser failed") - } - } - for _, _user := range sen.Users() { - if _user.Name == user.DefaultUserName { - ru := sentinelbuilder.GenerateSentinelDefaultRedisUser(sen.Definition(), sen.Definition().Spec.Auth.SecretPath) - oldRu, err := a.client.GetRedisUser(ctx, sen.GetNamespace(), ru.Name) - if err == nil && !slices.Equal(oldRu.Spec.PasswordSecrets, ru.Spec.PasswordSecrets) { - oldRu.Spec.PasswordSecrets = ru.Spec.PasswordSecrets - ru = *oldRu - if err := a.client.UpdateRedisUser(ctx, &ru); err != nil { - a.logger.Error(err, "update default user redisUser failed") - } - } else if errors.IsNotFound(err) { - if err := a.client.CreateRedisUser(ctx, &ru); err != nil { - a.logger.Error(err, "create default user redisUser failed") - } - } - } - if _user.Name == user.DefaultOperatorUserName { - if _user.Password == nil { - return actor.NewResultWithError(sentinel.CommandRequeue, fmt.Errorf("operator user password is nil")) - } - ru := sentinelbuilder.GenerateSentinelOperatorsRedisUser(sen, _user.Password.SecretName) - oldRu, err := a.client.GetRedisUser(ctx, sen.GetNamespace(), ru.Name) - if err == nil && !slices.Equal(oldRu.Spec.PasswordSecrets, ru.Spec.PasswordSecrets) { - oldRu.Spec.PasswordSecrets = ru.Spec.PasswordSecrets - ru = *oldRu - if err := a.client.UpdateRedisUser(ctx, &ru); err != nil { - a.logger.Error(err, "update default user redisUser failed") - } - } else if errors.IsNotFound(err) { - if err := a.client.CreateRedisUser(ctx, &ru); err != nil { - a.logger.Error(err, "create operator user redisUser failed") - } - } - } - - } - } else if sen.Version().IsACLSupported() && isAllACLSupported { - margs := [][]interface{}{} - margs = append( - margs, - []interface{}{"config", "set", "masteruser", sen.Users().GetOpUser().Name}, - []interface{}{"config", "set", "masterauth", sen.Users().GetOpUser().Password}, - ) - for _, node := range sen.Nodes() { - if node.ContainerStatus() == nil || !node.ContainerStatus().Ready || - node.IsTerminating() { - continue - } - - if err := node.Setup(ctx, margs...); err != nil { - a.logger.Error(err, "update acl config failed") - } - } - // then requeue to refresh cluster info - a.logger.Info("requeue to refresh cluster info") - return actor.NewResultWithValue(sentinel.CommandRequeue, time.Second) - } else if sen.Version().IsACLSupported() && !isAllACLSupported && isAllPodReady { - secret := sentinelbuilder.NewSentinelOpSecret(sen.Definition()) - if err := a.client.CreateIfNotExistsSecret(ctx, sen.GetNamespace(), secret); err != nil { - a.logger.Info("create operator secret", "secret", secret) - } - users := sen.Users() - if _, ok := users.Encode()[user.DefaultOperatorUserName]; !ok { - opUser, err := user.NewOperatorUser(secret, sen.Version().IsACL2Supported()) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - users = append(users, opUser) - data := users.Encode() - err = a.client.CreateOrUpdateConfigMap(ctx, sen.GetNamespace(), sentinelbuilder.NewSentinelAclConfigMap(sen.Definition(), data)) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - } else { - password := "" - secret := &corev1.Secret{} - if sen.Definition().Spec.Auth.SecretPath != "" { - _secret, err := a.client.GetSecret(ctx, sen.GetNamespace(), sen.Definition().Spec.Auth.SecretPath) - if err == nil { - secret = _secret - password = string(secret.Data["password"]) - } else if !errors.IsNotFound(err) { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - updateMasterAuth := []interface{}{"config", "set", "masterauth", password} - updateRequirePass := []interface{}{"config", "set", "requirepass", password} - for _, node := range sen.Nodes() { - if node.ContainerStatus() == nil || !node.ContainerStatus().Ready || - node.IsTerminating() { - continue - } - - if err := node.Setup(ctx, updateMasterAuth); err != nil { - a.logger.Error(err, "") - } - if err := node.Setup(ctx, updateRequirePass); err != nil { - a.logger.Error(err, "") - } - - cmd := []string{"sh", "-c", fmt.Sprintf(`echo -n '%s' > /tmp/newpass`, password)} - if !node.IsReady() || node.IsTerminating() { - continue - } - - // Retry hard - if err := util.RetryOnTimeout(func() error { - _, _, err := a.client.Exec(ctx, node.GetNamespace(), node.GetName(), clusterbuilder.ServerContainerName, cmd) - return err - }, 5); err != nil { - a.logger.Error(err, "patch new secret to pod failed", "pod", node.GetName()) - } - } - users := sen.Users() - if sen.Definition().Spec.Auth.SecretPath != "" { - passwd, err := user.NewPassword(secret) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - users.GetDefaultUser().Password = passwd - } else { - users.GetDefaultUser().Password = nil - } - data := users.Encode() - err := a.client.CreateOrUpdateConfigMap(ctx, sen.GetNamespace(), sentinelbuilder.NewSentinelAclConfigMap(sen.Definition(), data)) - if err != nil { - return actor.NewResultWithError(sentinel.CommandRequeue, err) - } - } - - return actor.NewResult(sentinel.CommandEnsureResource) -} - -func formatACLSetCommand(u *user.User) (args []interface{}) { - if u == nil { - return nil - } - if len(u.Rules) == 0 { - _ = u.AppendRule(&user.Rule{ - Categories: []string{"all"}, - KeyPatterns: []string{"*"}, - }) - } - // keep in mind that the user.Name is "default" for default user - // when update command,password,keypattern, must reset them all - args = append(args, "acl", "setuser", u.Name, "reset") - for _, rule := range u.Rules { - for _, cate := range rule.Categories { - cate = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(cate), "+"), "@") - args = append(args, fmt.Sprintf("+@%s", cate)) - } - for _, cmd := range rule.AllowedCommands { - cmd = strings.TrimPrefix(cmd, "+") - args = append(args, fmt.Sprintf("+%s", cmd)) - } - - isDisableAllCmd := false - for _, cmd := range rule.DisallowedCommands { - cmd = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(cmd), "-"), "@") - if cmd == "nocommands" || cmd == "-@all" { - isDisableAllCmd = true - } - args = append(args, fmt.Sprintf("-%s", cmd)) - } - if len(rule.Categories) == 0 && len(rule.AllowedCommands) == 0 && !isDisableAllCmd { - args = append(args, "+@all") - } - - if len(rule.KeyPatterns) == 0 { - rule.KeyPatterns = append(rule.KeyPatterns, "*") - } - for _, pattern := range rule.KeyPatterns { - pattern = strings.TrimPrefix(strings.TrimSpace(pattern), "~") - // Reference: https://raw.githubusercontent.com/antirez/redis/7.0/redis.conf - if !strings.HasPrefix(pattern, "%") { - pattern = fmt.Sprintf("~%s", pattern) - } - args = append(args, pattern) - } - for _, pattern := range rule.Channels { - pattern = strings.TrimPrefix(strings.TrimSpace(pattern), "&") - // Reference: https://raw.githubusercontent.com/antirez/redis/7.0/redis.conf - if !strings.HasPrefix(pattern, "&") { - pattern = fmt.Sprintf("&%s", pattern) - } - args = append(args, pattern) - } - - passwd := u.Password.String() - if passwd == "" { - args = append(args, "nopass") - } else { - args = append(args, fmt.Sprintf(">%s", passwd)) - } - - // NOTE: on must after reset - args = append(args, "on") - } - return -} diff --git a/pkg/ops/sentinel/command.go b/pkg/ops/sentinel/command.go deleted file mode 100644 index 6614497..0000000 --- a/pkg/ops/sentinel/command.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinel - -import "github.com/alauda/redis-operator/pkg/actor" - -type sentinelCommand struct { - typ string -} - -func (c *sentinelCommand) String() string { - if c == nil { - return "" - } - return c.typ -} - -var ( - CommandUpdateAccount actor.Command = &sentinelCommand{typ: "SentinelCommandUpdateAccount"} - CommandUpdateConfig = &sentinelCommand{typ: "SentinelCommandUpdateConfig"} - CommandEnsureResource = &sentinelCommand{typ: "SentinelCommandEnsureResource"} - CommandHealPod = &sentinelCommand{typ: "SentinelCommandHealPod"} - CommandCleanResource = &sentinelCommand{typ: "SentinelCommandCleanResource"} - - CommandSentinelHeal = &sentinelCommand{typ: "SentinelCommandSentinelHeal"} - CommandInitMaster = &sentinelCommand{typ: "SentinelCommandInitMaster"} - CommandHealMaster = &sentinelCommand{typ: "SentinelCommandHealMaster"} - CommandPatchLabels = &sentinelCommand{typ: "SentinelCommandPatchLabels"} - - CommandRequeue = &sentinelCommand{typ: "SentinelCommandRequeue"} - CommandAbort = &sentinelCommand{typ: "SentinelCommandAbort"} - CommandPaused = &sentinelCommand{typ: "SentinelCommandPaused"} -) diff --git a/pkg/ops/sentinel/engine.go b/pkg/ops/sentinel/engine.go deleted file mode 100644 index 42e2386..0000000 --- a/pkg/ops/sentinel/engine.go +++ /dev/null @@ -1,284 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package sentinel - -import ( - "context" - "fmt" - - "github.com/alauda/redis-operator/pkg/actor" - "github.com/alauda/redis-operator/pkg/config" - "github.com/alauda/redis-operator/pkg/kubernetes" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/clusterbuilder" - "github.com/alauda/redis-operator/pkg/kubernetes/builder/sentinelbuilder" - "github.com/alauda/redis-operator/pkg/types" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/user" - "github.com/alauda/redis-operator/pkg/util" - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/client-go/tools/record" -) - -type RuleEngine struct { - client kubernetes.ClientSet - eventRecorder record.EventRecorder - logger logr.Logger -} - -func NewRuleEngine(client kubernetes.ClientSet, eventRecorder record.EventRecorder, logger logr.Logger) (*RuleEngine, error) { - if client == nil { - return nil, fmt.Errorf("require client set") - } - if eventRecorder == nil { - return nil, fmt.Errorf("require EventRecorder") - } - - ctrl := RuleEngine{ - client: client, - eventRecorder: eventRecorder, - logger: logger, - } - return &ctrl, nil -} - -func (g *RuleEngine) Inspect(ctx context.Context, val types.RedisInstance) *actor.ActorResult { - if g == nil { - return nil - } - sentinel := val.(types.RedisFailoverInstance) - - logger := g.logger.WithName("Inspect").WithValues("namespace", sentinel.GetNamespace(), "name", sentinel.GetName()) - if sentinel == nil { - logger.Info("failover is nil") - return nil - } - cr := sentinel.Definition() - - if (cr.Spec.Redis.PodAnnotations != nil) && cr.Spec.Redis.PodAnnotations[config.PAUSE_ANNOTATION_KEY] != "" { - return actor.NewResult(CommandEnsureResource) - } - logger.V(3).Info("Inspecting sentinel") - - check_password := func() *actor.ActorResult { - logger.V(3).Info("check_password") - return g.CheckRedisPasswordChange(ctx, sentinel) - } - - if ret := check_password(); ret != nil { - logger.V(3).Info("check_password", "result", ret) - return ret - } - - if len(sentinel.Nodes()) == 0 || len(sentinel.SentinelNodes()) == 0 { - logger.Info("sentinel has no nodes,ensure resource") - return actor.NewResult(CommandEnsureResource) - } - - //first: check labels - check_2 := func() *actor.ActorResult { - logger.V(3).Info("check_labels") - return g.CheckRedisLabels(ctx, sentinel) - } - if ret := check_2(); ret != nil { - logger.V(3).Info("check_labels", "result", ret) - return ret - } - - // check only one master - check_0 := func() *actor.ActorResult { - logger.V(3).Info("check_only_one_master") - return g.CheckMaster(ctx, sentinel) - } - if ret := check_0(); ret != nil { - logger.V(3).Info("check_only_one_master", "result", ret) - return ret - } - - // check sentinel - check_1 := func() *actor.ActorResult { - logger.V(3).Info("check_sentinel") - return g.CheckSentinel(ctx, sentinel) - } - if ret := check_1(); ret != nil { - logger.V(3).Info("check_sentinel", "result", ret) - return ret - } - - check_configmap := func() *actor.ActorResult { - logger.V(3).Info("check_configmap") - return g.CheckRedisConfig(ctx, sentinel) - } - if ret := check_configmap(); ret != nil { - logger.V(3).Info("check_configmap", "result", ret) - return ret - } - - check_finally := func() *actor.ActorResult { - logger.V(3).Info("check_finnaly") - return g.FinallyCheck(ctx, sentinel) - } - if ret := check_finally(); ret != nil { - logger.V(3).Info("check_finnaly", "result", ret) - return ret - } - - logger.Info("sentinel inspect is healthy", "sentinel", sentinel.GetName(), "namespace", sentinel.GetNamespace()) - return nil - -} - -func (g *RuleEngine) CheckRedisLabels(ctx context.Context, st types.RedisInstance) *actor.ActorResult { - if len(st.Masters()) != 1 { - return nil - } - for _, v := range st.Nodes() { - labels := v.Definition().GetLabels() - if len(labels) > 0 { - switch v.Role() { - case redis.RedisRoleMaster: - if labels[sentinelbuilder.RedisRoleLabel] != sentinelbuilder.RedisRoleMaster { - g.logger.Info("master labels not match", "node", v.GetName(), "labels", labels) - return actor.NewResult(CommandPatchLabels) - } - - case redis.RedisRoleSlave: - if labels[sentinelbuilder.RedisRoleLabel] != sentinelbuilder.RedisRoleSlave { - g.logger.Info("slave labels not match", "node", v.GetName(), "labels", labels) - return actor.NewResult(CommandPatchLabels) - } - } - } - } - return nil -} - -// 检测是否只有一个master -func (g *RuleEngine) CheckMaster(ctx context.Context, st types.RedisFailoverInstance) *actor.ActorResult { - if len(st.Nodes()) == 0 { - return actor.NewResult(CommandEnsureResource) - } - - if len(st.Masters()) == 0 { - - return actor.NewResult(CommandInitMaster) - } - // 如果脑裂根据sentienl 选最优 - if len(st.Masters()) > 1 { - g.logger.Info("master more than one", "sentinel", st.GetName()) - return actor.NewResultWithError(CommandHealMaster, fmt.Errorf("sentinel %s has more than one master", st.GetName())) - } - // 节点掉了,根据sentinel选最优 - for _, n := range st.Nodes() { - if n.Info().MasterLinkStatus == "down" && n.Role() == redis.RedisRoleSlave { - g.logger.Info("master link down", "sentinel", st.GetName(), "node", n.GetName()) - return actor.NewResult(CommandHealMaster) - } - - } - return nil -} - -func (g *RuleEngine) CheckSentinel(ctx context.Context, st types.RedisFailoverInstance) *actor.ActorResult { - rf := st.Definition() - if len(st.SentinelNodes()) == 0 { - return actor.NewResult(CommandEnsureResource) - } - for _, v := range st.SentinelNodes() { - if v.Info().SentinelMaster0.Status != "ok" { - g.logger.Info("sentinel status not ok", "sentinel", v.GetName(), "status", v.Info().SentinelMaster0.Status) - return actor.NewResult(CommandSentinelHeal) - } - if v.Info().SentinelMaster0.Replicas != len(st.Nodes())-1 { - g.logger.Info("sentinel slaves not match", "sentinel", v.GetName(), "slaves", v.Info().SentinelMaster0.Replicas, "nodes", rf.Spec.Redis.Replicas) - return actor.NewResult(CommandHealMaster) - } - } - return nil -} - -func (g *RuleEngine) CheckRedisPasswordChange(ctx context.Context, st types.RedisFailoverInstance) *actor.ActorResult { - if st.Version().IsACLSupported() && !st.IsACLUserExists() { - return actor.NewResult(CommandUpdateAccount) - } - for _, v := range st.Users() { - if v.Name == user.DefaultUserName { - if v.Password == nil && st.Definition().Spec.Auth.SecretPath != "" { - return actor.NewResult(CommandUpdateAccount) - } - if v.Password != nil && v.Password.SecretName != st.Definition().Spec.Auth.SecretPath { - return actor.NewResult(CommandUpdateAccount) - } - } - if v.Name == user.DefaultOperatorUserName { - for _, vv := range st.Nodes() { - container := util.GetContainerByName(&vv.Definition().Spec, sentinelbuilder.ServerContainerName) - if container != nil && container.Env != nil { - for _, vc := range container.Env { - if vc.Name == sentinelbuilder.OperatorSecretName && vc.ValueFrom != nil { - if vc.ValueFrom.SecretKeyRef.Name != st.Definition().Spec.Auth.SecretPath { - return actor.NewResult(CommandUpdateAccount) - } - } - } - - } - } - if len(v.Rules) != 0 && len(v.Rules[0].Channels) == 0 && st.Version().IsACL2Supported() { - return actor.NewResult(CommandUpdateAccount) - } - } - } - return nil -} - -func (g *RuleEngine) CheckRedisConfig(ctx context.Context, st types.RedisFailoverInstance) *actor.ActorResult { - - newCm := sentinelbuilder.NewRedisConfigMap(st, st.Selector()) - oldCm, err := g.client.GetConfigMap(ctx, newCm.GetNamespace(), newCm.GetName()) - if errors.IsNotFound(err) || oldCm.Data[clusterbuilder.RedisConfKey] == "" { - return actor.NewResultWithError(CommandEnsureResource, fmt.Errorf("configmap %s not found", newCm.GetName())) - } else if err != nil { - return actor.NewResultWithError(CommandRequeue, err) - } - newConf, _ := clusterbuilder.LoadRedisConfig(newCm.Data[clusterbuilder.RedisConfKey]) - oldConf, _ := clusterbuilder.LoadRedisConfig(oldCm.Data[clusterbuilder.RedisConfKey]) - added, changed, deleted := oldConf.Diff(newConf) - if len(added)+len(changed)+len(deleted) != 0 { - return actor.NewResult(CommandUpdateConfig) - } - return nil -} - -// 最后比对数量是否相同 -func (g *RuleEngine) FinallyCheck(ctx context.Context, st types.RedisFailoverInstance) *actor.ActorResult { - rf := st.Definition() - for _, n := range st.Nodes() { - if n.Role() == redis.RedisRoleMaster && n.Info().ConnectedReplicas != int64(st.Definition().Spec.Redis.Replicas-1) { - g.logger.Info("master slaves not match", "sentinel", st.GetName(), "slaves", n.Info().ConnectedReplicas, "replicas", rf.Spec.Redis.Replicas) - return actor.NewResult(CommandHealMaster) - } - } - for _, v := range st.SentinelNodes() { - if v.Info().SentinelMaster0.Sentinels != int(st.Definition().Spec.Sentinel.Replicas) { - g.logger.Info("sentinel sentinels not match", "sentinel", v.GetName(), "sentinels", v.Info().SentinelMaster0.Sentinels, "replicas", rf.Spec.Sentinel.Replicas) - return actor.NewResult(CommandSentinelHeal) - } - } - - return nil -} diff --git a/pkg/redis/node.go b/pkg/redis/node.go index 5d9f58e..3cdff59 100644 --- a/pkg/redis/node.go +++ b/pkg/redis/node.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,24 +17,47 @@ limitations under the License. package redis import ( + "encoding/json" "fmt" "regexp" "strconv" "strings" + + "github.com/alauda/redis-operator/pkg/slot" +) + +const ( + MasterRole = "master" + SlaveRole = "slave" ) +type ClusterNodeAuxFields struct { + ShardID string `json:"shard-id"` + NodeName string `json:"nodename"` + TCPPort int64 `json:"tcp-port"` + TLSPort int64 `json:"tls-port"` + raw string +} + +func (c *ClusterNodeAuxFields) Raw() string { + return c.raw +} + type ClusterNode struct { Id string Addr string RawFlag string + BusPort string + AuxFields ClusterNodeAuxFields + Role string MasterId string PingSend int64 PongRecv int64 Epoch int64 LinkState string - Slots []string - Role string + slots []string + rawInfo string } func (n *ClusterNode) IsSelf() bool { @@ -44,48 +67,194 @@ func (n *ClusterNode) IsSelf() bool { return strings.HasPrefix(n.RawFlag, "myself") } +func (n *ClusterNode) IsFailed() bool { + if n == nil { + return true + } + return strings.Contains(n.RawFlag, "fail") +} + +func (n *ClusterNode) IsConnected() bool { + if n == nil { + return false + } + return n.LinkState == "connected" +} + +func (n *ClusterNode) IsJoined() bool { + if n == nil { + return false + } + return n.Addr != "" +} + +func (n *ClusterNode) Slots() *slot.Slots { + if n == nil { + return nil + } + if n.Role == SlaveRole || len(n.slots) == 0 { + return nil + } + + slots := slot.NewSlots() + _ = slots.Load(n.slots) + + return slots +} + +func (n *ClusterNode) Raw() string { + if n == nil { + return "" + } + return n.rawInfo +} + +type ClusterNodes []*ClusterNode + +func (ns ClusterNodes) Get(id string) *ClusterNode { + for _, n := range ns { + if n.Id == id { + return n + } + } + return nil +} + +func (ns ClusterNodes) Self() *ClusterNode { + for _, n := range ns { + if n.IsSelf() { + return n + } + } + return nil +} + +func (ns ClusterNodes) Replicas(id string) (ret []*ClusterNode) { + if len(ns) == 0 { + return + } + for _, n := range ns { + if n.MasterId == id { + ret = append(ret, n) + } + } + return +} + +func (ns ClusterNodes) Masters() (ret []*ClusterNode) { + for _, n := range ns { + if n.Role == MasterRole && len(n.slots) > 0 { + ret = append(ret, n) + } + } + return +} + +func (ns ClusterNodes) Marshal() ([]byte, error) { + data := []map[string]string{} + for _, n := range ns { + d := map[string]string{} + d["id"] = n.Id + d["addr"] = n.Addr + d["bus_port"] = n.BusPort + if data, _ := json.Marshal(&n.AuxFields); data != nil { + d["aux_fields"] = string(data) + } + d["flags"] = n.RawFlag + d["role"] = n.Role + d["master_id"] = n.MasterId + d["ping_send"] = strconv.FormatInt(n.PingSend, 10) + d["pong_recv"] = strconv.FormatInt(n.PongRecv, 10) + d["config_epoch"] = strconv.FormatInt(n.Epoch, 10) + d["link_state"] = n.LinkState + d["slots"] = strings.Join(n.slots, ",") + data = append(data, d) + } + return json.Marshal(data) +} + var ( invalidAddrReg = regexp.MustCompile(`^:\d+$`) ) +// ParseNodeFromClusterNode +// format: +// +// ... func ParseNodeFromClusterNode(line string) (*ClusterNode, error) { fields := strings.Fields(line) if len(fields) < 8 { return nil, fmt.Errorf("invalid node info %v", fields) } - n := ClusterNode{Id: fields[0]} - // if the node has join with other node, the addres is empty - if index := strings.Index(fields[1], "@"); index > 0 { - n.Addr = fields[1][0:index] - if invalidAddrReg.MatchString(n.Addr) { - n.Addr = "" + aux := ClusterNodeAuxFields{ + raw: fields[1], + } + auxFields := strings.Split(fields[1], ",") + addrPair := strings.SplitN(auxFields[0], "@", 2) + if len(addrPair) != 2 { + return nil, fmt.Errorf("invalid node info %v", fields) + } + if invalidAddrReg.MatchString(addrPair[0]) { + addrPair[0] = "" + } + for _, af := range auxFields[1:] { + kv := strings.Split(af, "=") + if len(kv) != 2 { + continue + } + switch kv[0] { + case "shard-id": + aux.ShardID = kv[1] + case "nodename": + aux.NodeName = kv[1] + case "tcp-port": + aux.TCPPort, _ = strconv.ParseInt(kv[1], 10, 64) + case "tls-port": + aux.TLSPort, _ = strconv.ParseInt(kv[1], 10, 64) } } - n.RawFlag = fields[2] - if fields[3] != "-" { - n.MasterId = fields[3] + role := fields[2] + if strings.Contains(fields[2], SlaveRole) { + role = SlaveRole + } else if strings.Contains(fields[2], MasterRole) { + role = MasterRole } + pingSend, _ := strconv.ParseInt(fields[4], 10, 64) + pongRecv, _ := strconv.ParseInt(fields[5], 10, 64) + epoch, _ := strconv.ParseInt(fields[6], 10, 64) - n.PingSend, _ = strconv.ParseInt(fields[4], 10, 64) - n.PongRecv, _ = strconv.ParseInt(fields[5], 10, 64) - n.Epoch, _ = strconv.ParseInt(fields[6], 10, 64) - n.LinkState = fields[7] - if len(fields) > 8 { - // TODO: parse slots - n.Slots = append(n.Slots, fields[8:]...) + node := &ClusterNode{ + Id: fields[0], + Addr: addrPair[0], + BusPort: addrPair[1], + AuxFields: aux, + RawFlag: fields[2], + Role: role, + MasterId: strings.TrimPrefix(fields[3], "-"), + PingSend: pingSend, + PongRecv: pongRecv, + Epoch: epoch, + LinkState: fields[7], + slots: fields[8:], + rawInfo: line, } - return &n, nil + return node, nil } -type ClusterNodes []*ClusterNode - -func (ns ClusterNodes) Self() *ClusterNode { - for _, n := range ns { - if n.IsSelf() { - return n +// parseNodes +func ParseNodes(data string) (nodes ClusterNodes, err error) { + lines := strings.Split(data, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "vars") { + continue + } + if node, err := ParseNodeFromClusterNode(line); err != nil { + return nil, err + } else { + nodes = append(nodes, node) } } - return nil + return } diff --git a/pkg/redis/node_test.go b/pkg/redis/node_test.go index 7fd561b..b27f31c 100644 --- a/pkg/redis/node_test.go +++ b/pkg/redis/node_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -60,3 +60,76 @@ func TestParseNodeFromClusterNode(t *testing.T) { }) } } + +func TestClusterNode_IsSelf(t *testing.T) { + tests := []struct { + name string + fields ClusterNode + want bool + }{ + { + name: "isSelf", + fields: ClusterNode{ + Id: "33b1262d41a4d9c27a78eef522c84999b064ce7f", + Addr: "", + RawFlag: "myself,master", + MasterId: "", + PingSend: 0, + PongRecv: 0, + Epoch: 0, + LinkState: "connected", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := &ClusterNode{ + Id: tt.fields.Id, + Addr: tt.fields.Addr, + RawFlag: tt.fields.RawFlag, + MasterId: tt.fields.MasterId, + PingSend: tt.fields.PingSend, + PongRecv: tt.fields.PongRecv, + Epoch: tt.fields.Epoch, + LinkState: tt.fields.LinkState, + slots: tt.fields.slots, + Role: tt.fields.Role, + } + if got := n.IsSelf(); got != tt.want { + t.Errorf("ClusterNode.IsSelf() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClusterNodes_Self(t *testing.T) { + node := ClusterNode{ + Id: "33b1262d41a4d9c27a78eef522c84999b064ce7f", + Addr: "", + RawFlag: "myself,master", + MasterId: "", + PingSend: 0, + PongRecv: 0, + Epoch: 0, + LinkState: "connected", + } + tests := []struct { + name string + ns ClusterNodes + want *ClusterNode + }{ + { + name: "self", + ns: []*ClusterNode{&node}, + want: &node, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ns.Self(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ClusterNodes.Self() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/redis/redis.go b/pkg/redis/redis.go index 30155c8..8ebb9ff 100644 --- a/pkg/redis/redis.go +++ b/pkg/redis/redis.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -28,90 +28,149 @@ import ( "github.com/gomodule/redigo/redis" ) -// RedisInfo -type RedisInfo struct { - RedisVersion string `json:"redis_version"` - RedisMode string `json:"redis_mode"` - RunId string `json:"run_id"` - UptimeInSeconds string `json:"uptime_in_seconds"` - AOFEnabled string `json:"aof_enabled"` - Role string `json:"role"` - MasterHost string `json:"master_host"` - MasterPort string `json:"master_port"` - ClusterEnabled string `json:"cluster_enabled"` - MasterLinkStatus string `json:"master_link_status"` - MasterReplOffset int64 `json:"master_repl_offset"` - UsedMemoryDataset int64 `json:"used_memory_dataset"` - SentinelMasters int64 `json:"sentinel_masters"` - SentinelTiLt int64 `json:"sentinel_tilt"` - SentinelRunningScript int64 `json:"sentinel_running_scripts"` - SentinelMaster0 SentinelMasterInfo `json:"master0"` - ConnectedReplicas int64 `json:"connected_slaves"` -} - -type SentinelMasterInfo struct { - Name string `json:"name"` - Status string `json:"status"` - Address Address `json:"address"` - Replicas int `json:"slaves"` - Sentinels int `json:"sentinels"` -} +type Address string -func parseIPAndPort(ipPortString string) (net.IP, int, error) { - // 查找最后一个冒号,将其之前的部分作为IP地址,之后的部分作为端口号 - lastColonIndex := strings.LastIndex(ipPortString, ":") +func (a Address) parse() (string, int, error) { + addr := string(a) + lastColonIndex := strings.LastIndex(addr, ":") if lastColonIndex == -1 { - return nil, 0, fmt.Errorf("Invalid IP:Port format") - } - - ipStr := ipPortString[:lastColonIndex] - portStr := ipPortString[lastColonIndex+1:] - - // 解析IP地址 - ip := net.ParseIP(ipStr) - if ip == nil { - return nil, 0, fmt.Errorf("Invalid IP address") + return "", 0, fmt.Errorf("Invalid IP:Port format") } - // 解析端口号 - portNumber, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("[%s]:%s", ip.String(), portStr)) + ip := strings.TrimSuffix(strings.TrimPrefix(addr[:lastColonIndex], "["), "]") + port, err := strconv.Atoi(addr[lastColonIndex+1:]) if err != nil { - return nil, 0, fmt.Errorf("Invalid port number: %v", err) + return "", 0, fmt.Errorf("Invalid port number: %v", err) } + return ip, port, nil +} - return ip, portNumber.Port, nil +func (a Address) Host() string { + host, _, _ := a.parse() + return host } -type Address string +func (a Address) Port() int { + _, port, _ := a.parse() + return port +} -func (a Address) Host() string { - address, _, err := parseIPAndPort(string(a)) - if err != nil { +func (a Address) String() string { + host, port, _ := a.parse() + return net.JoinHostPort(host, strconv.Itoa(port)) +} + +// RedisInfo +type RedisInfo struct { + RedisVersion string `json:"redis_version"` + RedisMode string `json:"redis_mode"` + RunId string `json:"run_id"` + UptimeInSeconds int64 `json:"uptime_in_seconds"` + AOFEnabled string `json:"aof_enabled"` + Role string `json:"role"` + ConnectedReplicas int64 `json:"connected_slaves"` + MasterHost string `json:"master_host"` + MasterPort string `json:"master_port"` + ClusterEnabled string `json:"cluster_enabled"` + MasterLinkStatus string `json:"master_link_status"` + MasterReplId string `json:"master_replid"` + MasterReplOffset int64 `json:"master_repl_offset"` + UsedMemory int64 `json:"used_memory"` + UsedMemoryDataset int64 `json:"used_memory_dataset"` + SentinelMasters int64 `json:"sentinel_masters"` + SentinelTiLt int64 `json:"sentinel_tilt"` + SentinelRunningScript int64 `json:"sentinel_running_scripts"` + SentinelMaster0 struct { + Name string `json:"name"` + Status string `json:"status"` + Address Address `json:"address"` + Replicas int `json:"slaves"` + Sentinels int `json:"sentinels"` + MonitorReplicas []SentinelMonitorNode `json:"monitor_replicas"` + } `json:"master0"` +} + +type SentinelMonitorNode struct { + Name string `json:"name"` + IP string `json:"ip"` + Port string `json:"port"` + RunId string `json:"run_id"` + Flags string `json:"flags"` + LinkPendingCommands int32 `json:"link_pending_commands"` + LinkRefcount int32 `json:"link_refcount"` + LastPingSent int64 `json:"last_ping_sent"` + LastOkPingReply int64 `json:"last_ok_ping_reply"` + LastPingReply int64 `json:"last_ping_reply"` + SDownTime int64 `json:"s_down_time"` + ODownTime int64 `json:"o_down_time"` + DownAfterMilliseconds int64 `json:"down_after_milliseconds"` + InfoRefresh int64 `json:"info_refresh"` + RoleReported string `json:"role_reported"` + RoleReportedTime int64 `json:"role_reported_time"` + ConfigEpoch int64 `json:"config_epoch"` + NumSlaves int32 `json:"num_slaves"` + NumOtherSentinels int32 `json:"num_other_sentinels"` + Quorum int32 `json:"quorum"` + FailoverTimeout int64 `json:"failover_timeout"` + ParallelSyncs int32 `json:"parallel_syncs"` + + // replica fields + MasterLinkDownTime int64 `json:"master_link_down_time"` + MasterLinkStatus string `json:"master_link_status"` + MasterHost string `json:"master_host"` + MasterPort string `json:"master_port"` + SlavePriority int32 `json:"slave_priority"` + SlaveReplOffset int64 `json:"slave_repl_offset"` + + // sentinel node specific fields + LastHelloMessage string `json:"last_hello_message"` + VotedLeader string `json:"voted_leader"` + VotedLeaderEpoch int64 `json:"voted_leader_epoch"` +} + +func (s *SentinelMonitorNode) Address() string { + if s == nil || s.IP == "" || s.Port == "" { return "" } - return address.String() + return net.JoinHostPort(s.IP, s.Port) } -func (a Address) Port() int { - _, port, err := parseIPAndPort(string(a)) - if err != nil { - return 0 +func (s *SentinelMonitorNode) IsMaster() bool { + if s == nil { + return false } - return port + return s.Flags == "master" } -func (a Address) ToString() string { - return string(a) +const ( + ClusterStateOk string = "ok" + ClusterStateFail string = "fail" +) + +type RedisClusterInfo struct { + ClusterState string `json:"cluster_state"` + ClusterSlotsAssigned int `json:"cluster_slots_assigned"` + ClusterSlotsOk int `json:"cluster_slots_ok"` + ClusterSlotsPfail int `json:"cluster_slots_pfail"` + ClusterSlotsFail int `json:"cluster_slots_fail"` + ClusterKnownNodes int `json:"cluster_known_nodes"` + ClusterSize int `json:"cluster_size"` + ClusterCurrentEpoch int `json:"cluster_current_epoch"` + ClusterMyEpoch int `json:"cluster_my_epoch"` } // RedisClient type RedisClient interface { Do(ctx context.Context, cmd string, args ...any) (any, error) - Pipelining(ctx context.Context, cmds []string, args [][]any) (interface{}, error) + DoWithTimeout(ctx context.Context, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) + Tx(ctx context.Context, cmds []string, args [][]any) (interface{}, error) + Pipeline(ctx context.Context, args [][]any) ([]PipelineResult, error) Close() error + Clone(ctx context.Context, addr string) RedisClient Ping(ctx context.Context) error Info(ctx context.Context) (*RedisInfo, error) + ClusterInfo(ctx context.Context) (*RedisClusterInfo, error) ConfigGet(ctx context.Context, cate string) (map[string]string, error) ConfigSet(ctx context.Context, params map[string]string) error Nodes(ctx context.Context) (ClusterNodes, error) @@ -123,6 +182,8 @@ type AuthConfig struct { TLSConfig *tls.Config } +type AuthInfo = AuthConfig + type redisClient struct { authInfo *AuthConfig addr string @@ -149,6 +210,7 @@ func NewRedisClient(addr string, authInfo AuthConfig) RedisClient { redis.DialTLSSkipVerify(true), ) } + opts = append(opts, redis.DialClientName("redis-operator")) ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() @@ -186,8 +248,15 @@ func (c *redisClient) Do(ctx context.Context, cmd string, args ...any) (any, err return redis.DoContext(conn, ctx, cmd, args...) } -// Pipeline -func (c *redisClient) Pipelining(ctx context.Context, cmds []string, args [][]any) (interface{}, error) { +func (c *redisClient) DoWithTimeout(ctx context.Context, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + return c.Do(ctx, cmd, args...) +} + +// Tx +func (c *redisClient) Tx(ctx context.Context, cmds []string, args [][]any) (interface{}, error) { if c == nil || c.pool == nil { return nil, nil } @@ -209,14 +278,50 @@ func (c *redisClient) Pipelining(ctx context.Context, cmds []string, args [][]an return result, err } for _, v := range result { - switch v.(type) { + switch rv := v.(type) { case redis.Error: - return result, fmt.Errorf("redis error: %s", v) + return result, fmt.Errorf("redis error: %s", rv.Error()) } } return result, nil } +type PipelineResult struct { + Value any + Error error +} + +// Pipeline +func (c *redisClient) Pipeline(ctx context.Context, args [][]any) ([]PipelineResult, error) { + if c == nil || c.pool == nil { + return nil, nil + } + + conn := c.pool.Get() + defer conn.Close() + + for _, arg := range args { + cmd, ok := arg[0].(string) + if !ok { + return nil, fmt.Errorf("invalid command") + } + if err := conn.Send(cmd, arg[1:]...); err != nil { + return nil, err + } + } + if err := conn.Flush(); err != nil { + return nil, err + } + + var rets []PipelineResult + for i := 0; i < len(args); i++ { + ret := PipelineResult{} + ret.Value, ret.Error = conn.Receive() + rets = append(rets, ret) + } + return rets, nil +} + // Close func (c *redisClient) Close() error { if c == nil || c.pool == nil { @@ -225,6 +330,14 @@ func (c *redisClient) Close() error { return c.pool.Close() } +func (c *redisClient) Clone(ctx context.Context, addr string) RedisClient { + if c == nil || c.authInfo == nil { + return nil + } + + return NewRedisClient(addr, *c.authInfo) +} + // Ping func (c *redisClient) Ping(ctx context.Context) error { if c == nil || c.pool == nil { @@ -306,10 +419,7 @@ func (c *redisClient) Info(ctx context.Context) (*RedisInfo, error) { return nil, nil } - conn := c.pool.Get() - defer conn.Close() - - data, err := redis.String(redis.DoContext(conn, ctx, "INFO")) + data, err := redis.String(c.DoWithTimeout(ctx, time.Second*10, "INFO")) if err != nil { return nil, err } @@ -334,7 +444,8 @@ func (c *redisClient) Info(ctx context.Context) (*RedisInfo, error) { case "run_id": info.RunId = fields[1] case "uptime_in_seconds": - info.UptimeInSeconds = fields[1] + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.UptimeInSeconds = val case "aof_enabled": info.AOFEnabled = fields[1] case "role": @@ -347,9 +458,14 @@ func (c *redisClient) Info(ctx context.Context) (*RedisInfo, error) { info.ClusterEnabled = fields[1] case "master_link_status": info.MasterLinkStatus = fields[1] + case "master_replid": + info.MasterReplId = fields[1] case "master_repl_offset": val, _ := strconv.ParseInt(fields[1], 10, 64) info.MasterReplOffset = val + case "used_memory": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.UsedMemory = val case "used_memory_dataset": val, _ := strconv.ParseInt(fields[1], 10, 64) info.UsedMemoryDataset = val @@ -366,6 +482,7 @@ func (c *redisClient) Info(ctx context.Context) (*RedisInfo, error) { val, _ := strconv.ParseInt(fields[1], 10, 64) info.SentinelRunningScript = val case "master0": + info.Role = "sentinel" fields := strings.Split(fields[1], ",") if len(fields) != 5 { continue @@ -396,6 +513,157 @@ func (c *redisClient) Info(ctx context.Context) (*RedisInfo, error) { return parseInfo(data), nil } +func ParseSentinelMonitorNode(val interface{}) *SentinelMonitorNode { + kvs, _ := redis.StringMap(val, nil) + node := SentinelMonitorNode{} + for k, v := range kvs { + switch k { + case "name": + node.Name = v + case "ip": + node.IP = v + case "port": + node.Port = v + case "runid": + node.RunId = v + case "flags": + node.Flags = v + case "link-pending-commands": + iv, _ := strconv.ParseInt(v, 10, 32) + node.LinkPendingCommands = int32(iv) + case "link-refcount": + iv, _ := strconv.ParseInt(v, 10, 32) + node.LinkRefcount = int32(iv) + case "last-ping-sent": + iv, _ := strconv.ParseInt(v, 10, 64) + node.LastPingSent = iv + case "last-ok-ping-reply": + iv, _ := strconv.ParseInt(v, 10, 64) + node.LastOkPingReply = iv + case "last-ping-reply": + iv, _ := strconv.ParseInt(v, 10, 64) + node.LastPingReply = iv + case "s-down-time": + iv, _ := strconv.ParseInt(v, 10, 64) + node.SDownTime = iv + case "o-down-time": + iv, _ := strconv.ParseInt(v, 10, 64) + node.ODownTime = iv + case "down-after-milliseconds": + iv, _ := strconv.ParseInt(v, 10, 64) + node.DownAfterMilliseconds = iv + case "info-refresh": + iv, _ := strconv.ParseInt(v, 10, 64) + node.InfoRefresh = iv + case "role-reported": + node.RoleReported = v + case "role-reported-time": + iv, _ := strconv.ParseInt(v, 10, 64) + node.RoleReportedTime = iv + case "config-epoch": + iv, _ := strconv.ParseInt(v, 10, 64) + node.ConfigEpoch = iv + case "num-slaves": + iv, _ := strconv.ParseInt(v, 10, 32) + node.NumSlaves = int32(iv) + case "num-other-sentinels": + iv, _ := strconv.ParseInt(v, 10, 32) + node.NumOtherSentinels = int32(iv) + case "quorum": + iv, _ := strconv.ParseInt(v, 10, 32) + node.Quorum = int32(iv) + case "failover-timeout": + iv, _ := strconv.ParseInt(v, 10, 64) + node.FailoverTimeout = iv + case "parallel-syncs": + iv, _ := strconv.ParseInt(v, 10, 32) + node.ParallelSyncs = int32(iv) + case "master-link-down-time": + iv, _ := strconv.ParseInt(v, 10, 64) + node.MasterLinkDownTime = iv + case "master-link-status": + node.MasterLinkStatus = v + case "master-host": + node.MasterHost = v + case "master-port": + node.MasterPort = v + case "slave-priority": + iv, _ := strconv.ParseInt(v, 10, 32) + node.SlavePriority = int32(iv) + case "slave-repl-offset": + iv, _ := strconv.ParseInt(v, 10, 64) + node.SlaveReplOffset = iv + case "last-hello-message": + node.LastHelloMessage = v + case "voted-leader": + node.VotedLeader = v + case "voted-leader-epoch": + iv, _ := strconv.ParseInt(v, 10, 64) + node.VotedLeaderEpoch = iv + } + } + return &node +} + +func (c *redisClient) ClusterInfo(ctx context.Context) (*RedisClusterInfo, error) { + if c == nil || c.pool == nil { + return nil, nil + } + + conn := c.pool.Get() + defer conn.Close() + + data, err := redis.String(redis.DoContext(conn, ctx, "CLUSTER", "INFO")) + if err != nil { + return nil, err + } + + parseInfo := func(data string) *RedisClusterInfo { + info := RedisClusterInfo{} + lines := strings.Split(data, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + fields := strings.SplitN(line, ":", 2) + if len(fields) != 2 { + continue + } + switch fields[0] { + case "cluster_state": + info.ClusterState = fields[1] + case "cluster_slots_assigned": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterSlotsAssigned = int(val) + case "cluster_slots_ok": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterSlotsOk = int(val) + case "cluster_slots_pfail": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterSlotsPfail = int(val) + case "cluster_slots_fail": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterSlotsFail = int(val) + case "cluster_known_nodes": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterKnownNodes = int(val) + case "cluster_size": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterSize = int(val) + case "cluster_current_epoch": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterCurrentEpoch = int(val) + case "cluster_my_epoch": + val, _ := strconv.ParseInt(fields[1], 10, 64) + info.ClusterMyEpoch = int(val) + } + } + return &info + } + return parseInfo(data), nil +} + // Nodes func (c *redisClient) Nodes(ctx context.Context) (ClusterNodes, error) { if c == nil || c.pool == nil { diff --git a/pkg/redis/redis_test.go b/pkg/redis/redis_test.go index 141bfb5..db63fe5 100644 --- a/pkg/redis/redis_test.go +++ b/pkg/redis/redis_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -20,37 +20,38 @@ import ( "testing" ) -func TestParseIPAndPort(t *testing.T) { +func TestParseAddress(t *testing.T) { testCases := []struct { - input string - expectedIP string - expectedPort int - expectError bool + input string + expectedIP string + expectedPort int + exportedString string + expectError bool }{ - {"192.168.1.1:8080", "192.168.1.1", 8080, false}, - {"1335::172:168:200:5d1:32428", "1335::172:168:200:5d1", 32428, false}, - {"invalid-ip:port", "", 0, true}, + {"192.168.1.1:8080", "192.168.1.1", 8080, "192.168.1.1:8080", false}, + {"1335::172:168:200:5d1:32428", "1335::172:168:200:5d1", 32428, "[1335::172:168:200:5d1]:32428", false}, + {"::1:6379", "::1", 6379, "[::1]:6379", false}, + {":6379", "", 6379, ":6379", false}, + {"localhost:6379", "localhost", 6379, "localhost:6379", false}, + {"invalid-ip:port", "", 0, "", true}, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { - ip, port, err := parseIPAndPort(tc.input) - - if tc.expectError { - if err == nil { + addr := Address(tc.input) + if _, _, err := addr.parse(); err != nil { + if !tc.expectError { t.Errorf("Expected an error, but got nil") } } else { - if err != nil { - t.Errorf("Unexpected error: %v", err) + if addr.Host() != tc.expectedIP { + t.Errorf("Expected IP: %s, got: %s", tc.expectedIP, addr.Host()) } - - if ip.String() != tc.expectedIP { - t.Errorf("Expected IP: %s, got: %s", tc.expectedIP, ip.String()) + if addr.Port() != tc.expectedPort { + t.Errorf("Expected port: %d, got: %d", tc.expectedPort, addr.Port()) } - - if port != tc.expectedPort { - t.Errorf("Expected port: %d, got: %d", tc.expectedPort, port) + if addr.String() != tc.exportedString { + t.Errorf("Expected string: %s, got: %s", tc.exportedString, addr.String()) } } }) diff --git a/pkg/security/acl/user.go b/pkg/security/acl/user.go index 8aeaba2..397c913 100644 --- a/pkg/security/acl/user.go +++ b/pkg/security/acl/user.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -63,9 +63,9 @@ func LoadACLUsers(ctx context.Context, clientset kubernetes.ClientSet, cm *corev return users, nil } -func NewOperatorUser(ctx context.Context, clientset kubernetes.ClientSet, name, namespace string, ownerRefs []metav1.OwnerReference, ACL2Support bool) (*core.User, error) { +func NewOperatorUser(ctx context.Context, clientset kubernetes.ClientSet, secretName, namespace string, ownerRefs []metav1.OwnerReference, ACL2Support bool) (*core.User, error) { // get secret - oldSecret, _ := clientset.GetSecret(ctx, namespace, name) + oldSecret, _ := clientset.GetSecret(ctx, namespace, secretName) if oldSecret != nil { if data, ok := oldSecret.Data["password"]; ok && len(data) != 0 { return core.NewOperatorUser(oldSecret, ACL2Support) @@ -76,15 +76,16 @@ func NewOperatorUser(ctx context.Context, clientset kubernetes.ClientSet, name, if err != nil { return nil, fmt.Errorf("generate password for operator user failed, error=%s", err) } + secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: name, + Name: secretName, Namespace: namespace, OwnerReferences: ownerRefs, }, - StringData: map[string]string{ - "password": plainPasswd, - "username": user.DefaultOperatorUserName, + Data: map[string][]byte{ + "password": []byte(plainPasswd), + "username": []byte(user.DefaultOperatorUserName), }, } if err := clientset.CreateIfNotExistsSecret(ctx, namespace, &secret); err != nil { @@ -98,6 +99,10 @@ type Users []*core.User // GetByRole func (us Users) GetOpUser() *core.User { + if us == nil { + return nil + } + var commonUser *core.User for _, u := range us { if u.Role == core.RoleOperator { @@ -120,11 +125,17 @@ func (us Users) GetDefaultUser() *core.User { } // Encode -func (us Users) Encode() map[string]string { +func (us Users) Encode(patch bool) map[string]string { ret := map[string]string{} - for _, user := range us { - data, _ := json.Marshal(user) - ret[user.Name] = string(data) + for _, u := range us { + if len(u.Rules) == 0 { + u.Rules = append(u.Rules, &user.Rule{}) + } + if patch { + u.Rules[0] = user.PatchRedisClusterClientRequiredRules(u.Rules[0]) + } + data, _ := json.Marshal(u) + ret[u.Name] = string(data) } return ret } diff --git a/pkg/security/acl/user_test.go b/pkg/security/acl/user_test.go new file mode 100644 index 0000000..58ad90f --- /dev/null +++ b/pkg/security/acl/user_test.go @@ -0,0 +1,291 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package acl + +import ( + "context" + "testing" + + "github.com/alauda/redis-operator/pkg/kubernetes/clientset/mocks" + core "github.com/alauda/redis-operator/pkg/types/user" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestLoadACLUsers(t *testing.T) { + ctx := context.TODO() + + clientset := mocks.NewClientSet(t) + t.Run("nil ConfigMap", func(t *testing.T) { + users, err := LoadACLUsers(ctx, clientset, nil) + if err != nil { + t.Errorf("LoadACLUsers() error = %v, wantErr %v", err, false) + } + if len(users) != 0 { + t.Errorf("LoadACLUsers() = %v, want %v", len(users), 0) + } + }) + + t.Run("default name ConfigMap", func(t *testing.T) { + namespace := "default" + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: namespace, + }, + Data: map[string]string{ + "": `{"name":"","role":"Developer","rules":[{"categories":["all"],"disallowedCommands":["acl","flushall","flushdb","keys"],"keyPatterns":["*"]}]}`, + }, + } + users, err := LoadACLUsers(ctx, clientset, cm) + if err != nil { + t.Errorf("LoadACLUsers() error = %v, wantErr %v", err, false) + } + if len(users) != 1 { + t.Errorf("LoadACLUsers() = %v, want %v", len(users), 1) + } + if users[0].Name != core.DefaultUserName { + t.Errorf("LoadACLUsers() = %v, want %v", users[0].Name, core.DefaultUserName) + } + }) + + t.Run("valid ConfigMap", func(t *testing.T) { + secretName := "redis-sen-customport-sm8r5" + namespace := "default" + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "password": []byte("password"), + }, + } + clientset.On("GetSecret", ctx, namespace, secret.Name).Return(secret, nil) + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: namespace, + }, + Data: map[string]string{ + "default": `{"name":"default","role":"Developer","password":{"secretName":"redis-sen-customport-sm8r5"},"rules":[{"categories":["all"],"disallowedCommands":["acl","flushall","flushdb","keys"],"keyPatterns":["*"]}]}`, + }, + } + users, err := LoadACLUsers(ctx, clientset, cm) + if err != nil { + t.Errorf("LoadACLUsers() error = %v, wantErr %v", err, false) + } + if len(users) != 1 { + t.Errorf("LoadACLUsers() = %v, want %v", len(users), 1) + } + }) + + t.Run("invalid username", func(t *testing.T) { + cm := &corev1.ConfigMap{ + Data: map[string]string{ + "abc+123": `{"name":"abc+123","role":"Developer","rules":[{"categories":["all"],"disallowedCommands":["acl","flushall","flushdb","keys"],"keyPatterns":["*"]}]}`, + }, + } + _, err := LoadACLUsers(ctx, clientset, cm) + if err == nil { + t.Errorf("LoadACLUsers() error = %v, wantErr %v", err, true) + } + }) + + t.Run("invalid role", func(t *testing.T) { + cm := &corev1.ConfigMap{ + Data: map[string]string{ + "test": `{"name":"test","role":"abc","rules":[{"categories":["all"],"disallowedCommands":["acl","flushall","flushdb","keys"],"keyPatterns":["*"]}]}`, + }, + } + _, err := LoadACLUsers(ctx, clientset, cm) + if err == nil { + t.Errorf("LoadACLUsers() error = %v, wantErr %v", err, true) + } + }) + + t.Run("invalid ConfigMap", func(t *testing.T) { + cm := &corev1.ConfigMap{ + Data: map[string]string{ + "default": `invalid json`, + }, + } + _, err := LoadACLUsers(ctx, clientset, cm) + if err == nil { + t.Errorf("LoadACLUsers() error = %v, wantErr %v", err, true) + } + }) +} + +func TestNewOperatorUser(t *testing.T) { + ctx := context.TODO() + ownerRefs := []metav1.OwnerReference{} + + t.Run("existing secret", func(t *testing.T) { + namespace := "default" + secretName := "test-secret" + clientset := mocks.NewClientSet(t) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "password": []byte("password"), + }, + } + clientset.On("CreateSecret", ctx, namespace, secret).Return(nil) + clientset.On("GetSecret", ctx, namespace, secret.Name).Return(secret, nil) + if err := clientset.CreateSecret(ctx, namespace, secret); err != nil { + t.Errorf("CreateSecret() error = %v, wantErr %v", err, false) + } + user, err := NewOperatorUser(ctx, clientset, secretName, namespace, ownerRefs, false) + if err != nil { + t.Errorf("NewOperatorUser() error = %v, wantErr %v", err, false) + } + if user == nil { + t.Errorf("NewOperatorUser() = %v, want non-nil user", user) + } + }) + + t.Run("non-existing secret", func(t *testing.T) { + clientset := mocks.NewClientSet(t) + namespace := "default2" + secretName := "test-secret2" + statusErr := &errors.StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonNotFound}} + clientset.On("GetSecret", ctx, namespace, secretName).Return(nil, statusErr) + clientset.On("CreateIfNotExistsSecret", ctx, namespace, mock.Anything).Return(nil) + user, err := NewOperatorUser(ctx, clientset, secretName, namespace, ownerRefs, false) + if err != nil { + t.Errorf("NewOperatorUser() error = %v, wantErr %v", err, false) + } + if user == nil { + t.Errorf("NewOperatorUser() = %v, want non-nil user", user) + } + }) +} + +func TestUsers_GetOpUser(t *testing.T) { + t.Run("empty Users slice", func(t *testing.T) { + users := Users{} + if got := users.GetOpUser(); got != nil { + t.Errorf("Users.GetOpUser() = %v, want %v", got, nil) + } + }) + + t.Run("Users with operator", func(t *testing.T) { + users := Users{ + &core.User{Name: "op", Role: core.RoleOperator}, + } + if got := users.GetOpUser(); got == nil || got.Role != core.RoleOperator { + t.Errorf("Users.GetOpUser() = %v, want %v", got, core.RoleOperator) + } + }) + + t.Run("Users with only developers", func(t *testing.T) { + users := Users{ + &core.User{Name: "dev", Role: core.RoleDeveloper}, + } + if got := users.GetOpUser(); got == nil || got.Role != core.RoleDeveloper { + t.Errorf("Users.GetOpUser() = %v, want %v", got, core.RoleDeveloper) + } + }) +} + +func TestUsers_GetDefaultUser(t *testing.T) { + t.Run("empty Users slice", func(t *testing.T) { + users := Users{} + if got := users.GetDefaultUser(); got != nil { + t.Errorf("Users.GetDefaultUser() = %v, want %v", got, nil) + } + }) + + t.Run("Users with default user", func(t *testing.T) { + users := Users{ + &core.User{Name: core.DefaultUserName, Role: core.RoleDeveloper}, + } + if got := users.GetDefaultUser(); got == nil || got.Name != core.DefaultUserName { + t.Errorf("Users.GetDefaultUser() = %v, want %v", got, core.DefaultUserName) + } + }) + + t.Run("Users with no default user", func(t *testing.T) { + users := Users{ + &core.User{Name: "custom", Role: core.RoleDeveloper}, + } + if got := users.GetDefaultUser(); got != nil { + t.Errorf("Users.GetDefaultUser() = %v, want %v", got, nil) + } + }) +} + +func TestUsers_Encode(t *testing.T) { + t.Run("empty Users slice", func(t *testing.T) { + users := Users{} + if got := users.Encode(false); len(got) != 0 { + t.Errorf("Users.Encode() = %v, want %v", got, map[string]string{}) + } + }) + + t.Run("Users with valid users", func(t *testing.T) { + users := Users{ + &core.User{Name: "user1", Role: core.RoleDeveloper}, + } + got := users.Encode(false) + if len(got) != 1 { + t.Errorf("Users.Encode() = %v, want %v", len(got), 1) + } + }) +} + +func TestUsers_IsChanged(t *testing.T) { + t.Run("different lengths", func(t *testing.T) { + users1 := Users{ + &core.User{Name: "user1", Role: core.RoleDeveloper}, + } + users2 := Users{} + if got := users1.IsChanged(users2); got != true { + t.Errorf("Users.IsChanged() = %v, want %v", got, true) + } + }) + + t.Run("identical Users slices", func(t *testing.T) { + users1 := Users{ + &core.User{Name: "user1", Role: core.RoleDeveloper}, + } + users2 := Users{ + &core.User{Name: "user1", Role: core.RoleDeveloper}, + } + if got := users1.IsChanged(users2); got != false { + t.Errorf("Users.IsChanged() = %v, want %v", got, false) + } + }) + + t.Run("different Users slices", func(t *testing.T) { + users1 := Users{ + &core.User{Name: "user1", Role: core.RoleDeveloper}, + } + users2 := Users{ + &core.User{Name: "user2", Role: core.RoleDeveloper}, + } + if got := users1.IsChanged(users2); got != true { + t.Errorf("Users.IsChanged() = %v, want %v", got, true) + } + }) +} diff --git a/pkg/security/password/password.go b/pkg/security/password/password.go index 994da38..9ff3ed0 100644 --- a/pkg/security/password/password.go +++ b/pkg/security/password/password.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/security/password/password_test.go b/pkg/security/password/password_test.go index 210b078..eaefbe5 100644 --- a/pkg/security/password/password_test.go +++ b/pkg/security/password/password_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -112,6 +112,41 @@ func TestPasswordValidate(t *testing.T) { }, wantErr: false, }, + { + name: "12345$%^&*", + args: args{ + pwd: "12345$%^&*", + }, + wantErr: true, + }, + { + name: "123admin==", + args: args{ + pwd: "123admin==", + }, + wantErr: false, + }, + { + name: "123456789012345678901234567890123", + args: args{ + pwd: "123456789012345678901234567890123", + }, + wantErr: true, + }, + { + name: "1234567", + args: args{ + pwd: "1234567", + }, + wantErr: true, + }, + { + name: "admin123", + args: args{ + pwd: "admin123", + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -121,3 +156,29 @@ func TestPasswordValidate(t *testing.T) { }) } } + +/* + // example + err := validatePasswordComplexity("12345$%^&*") + if err == nil { + t.Error("Expected an error for password without a letter, but got nil") + } + err = validatePasswordComplexity("123admin==") + if err != nil { + t.Errorf("Expected no error for a valid password, but got: %v", err) + } + err = validatePasswordComplexity("123456789012345678901234567890123") + if err == nil { + t.Error("Expected an error for password length greater than 32, but got nil") + } + err = validatePasswordComplexity("1234567") + if err == nil { + t.Error("Expected an error for password length less than 8, but got nil") + } + + err = validatePasswordComplexity("admin123") + if err == nil { + t.Error(err) + t.Error("Expected an error for password without a symbol, but got nil") + } +*/ diff --git a/pkg/types/slot/allocate.go b/pkg/slot/allocate.go similarity index 96% rename from pkg/types/slot/allocate.go rename to pkg/slot/allocate.go index 90297a6..311138b 100644 --- a/pkg/types/slot/allocate.go +++ b/pkg/slot/allocate.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -105,7 +105,7 @@ func Allocate(desired int, shards []*Slots) (ret []*Slots) { slots = slots[0 : len(slots)-count] for _, slot := range toMoveSlots { _ = ret[target].Set(slot, SlotAssigned) - _ = ret[i].Set(slot, SlotUnAssigned) + _ = ret[i].Set(slot, SlotUnassigned) } } } @@ -173,7 +173,7 @@ func Allocate(desired int, shards []*Slots) (ret []*Slots) { slots = slots[count:] for _, slot := range toMoveSlots { _ = ret[target].Set(slot, SlotAssigned) - _ = shards[i].Set(slot, SlotUnAssigned) + _ = shards[i].Set(slot, SlotUnassigned) } } } diff --git a/pkg/types/slot/allocate_test.go b/pkg/slot/allocate_test.go similarity index 98% rename from pkg/types/slot/allocate_test.go rename to pkg/slot/allocate_test.go index d7b41ab..68f7b9d 100644 --- a/pkg/types/slot/allocate_test.go +++ b/pkg/slot/allocate_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -24,7 +24,7 @@ import ( func testLoadSlots(v string, status SlotAssignStatus) *Slots { s := NewSlots() - s.Set(v, status) + _ = s.Set(v, status) return s } diff --git a/pkg/types/slot/slot.go b/pkg/slot/slot.go similarity index 86% rename from pkg/types/slot/slot.go rename to pkg/slot/slot.go index ab193a5..b59e123 100644 --- a/pkg/types/slot/slot.go +++ b/pkg/slot/slot.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,8 +19,11 @@ package slot import ( "errors" "fmt" + "sort" "strconv" "strings" + + "github.com/samber/lo" ) const ( @@ -31,8 +34,8 @@ const ( type SlotAssignStatus int const ( - // SlotUnAssigned - slot assigned - SlotUnAssigned SlotAssignStatus = 0 + // SlotUnassigned - slot assigned + SlotUnassigned SlotAssignStatus = 0 // SlotImporting - slot is in importing status SlotImporting SlotAssignStatus = 1 // SlotAssigned - slot is assigned @@ -62,7 +65,7 @@ func (s SlotAssignStatus) String() string { case SlotAssigned: return "SlotAssigned" } - return "SlotUnAssigned" + return "SlotUnassigned" } // Slots use two byte represent the slot status @@ -134,7 +137,7 @@ func (s *Slots) parseStrSlots(v string) (ret map[int]SlotAssignStatus, nodes map field = strings.TrimSuffix(strings.TrimPrefix(field, "["), "]") if strings.Contains(field, "-<-") { moveFields := strings.SplitN(field, "-<-", 2) - if len(moveFields) != 2 { + if len(moveFields) != 2 || len(moveFields[0]) == 0 || len(moveFields[1]) == 0 { return nil, nil, fmt.Errorf("invalid slot %s", field) } start, err := strconv.ParseInt(moveFields[0], 10, 32) @@ -145,7 +148,7 @@ func (s *Slots) parseStrSlots(v string) (ret map[int]SlotAssignStatus, nodes map nodes[int(start)] = moveFields[1] } else if strings.Contains(field, "->-") { moveFields := strings.SplitN(field, "->-", 2) - if len(moveFields) != 2 { + if len(moveFields) != 2 || len(moveFields[0]) == 0 || len(moveFields[1]) == 0 { return nil, nil, fmt.Errorf("invalid slot %s", field) } start, err := strconv.ParseInt(moveFields[0], 10, 32) @@ -156,9 +159,6 @@ func (s *Slots) parseStrSlots(v string) (ret map[int]SlotAssignStatus, nodes map nodes[int(start)] = moveFields[1] } else if strings.Contains(field, "-") { rangeFields := strings.SplitN(field, "-", 2) - if len(rangeFields) != 2 { - return nil, nil, fmt.Errorf("invalid range slot %s", field) - } start, err := strconv.ParseInt(rangeFields[0], 10, 32) if err != nil { return nil, nil, fmt.Errorf("invalid range slot %s", field) @@ -220,8 +220,11 @@ func (s *Slots) Load(v interface{}) error { if err != nil { return err } - for i, status := range ret { - if err = handler(i, status, nodes[i]); err != nil { + keys := lo.Keys(ret) + sort.Ints(keys) + for _, key := range keys { + status := ret[key] + if err = handler(key, status, nodes[key]); err != nil { return err } } @@ -230,8 +233,12 @@ func (s *Slots) Load(v interface{}) error { if err != nil { return err } - for i, status := range ret { - if err = handler(i, status, nodes[i]); err != nil { + + keys := lo.Keys(ret) + sort.Ints(keys) + for _, key := range keys { + status := ret[key] + if err = handler(key, status, nodes[key]); err != nil { return err } } @@ -350,13 +357,34 @@ func (s *Slots) Union(slots ...*Slots) *Slots { } // Slots -func (s *Slots) Slots() (ret []int) { +func (s *Slots) Slots(ss ...SlotAssignStatus) (ret []int) { if s == nil { return nil } + + if len(ss) == 0 { + ss = []SlotAssignStatus{SlotAssigned} + } for i := 0; i < RedisMaxSlots; i++ { index, offset := s.convertIndex(i) - if (s.data[index]>>offset)&uint8(SlotAssigned) > 0 { + for _, st := range ss { + if (s.data[index]>>offset)&uint8(SlotMigrating) == uint8(st) { + ret = append(ret, i) + break + } + } + } + return +} + +// SlotsByStatus +func (s *Slots) SlotsByStatus(status SlotAssignStatus) (ret []int) { + if s == nil { + return nil + } + for i := 0; i < RedisMaxSlots; i++ { + index, offset := s.convertIndex(i) + if (s.data[index]>>offset)&uint8(SlotMigrating) == uint8(status) { ret = append(ret, i) } } @@ -409,7 +437,7 @@ func (s *Slots) Count(status SlotAssignStatus) (c int) { } mask := uint8(SlotMigrating) - if status == SlotUnAssigned || status == SlotAssigned { + if status == SlotUnassigned || status == SlotAssigned { mask = uint8(SlotAssigned) } @@ -424,7 +452,7 @@ func (s *Slots) Count(status SlotAssignStatus) (c int) { func (s *Slots) Status(i int) SlotAssignStatus { if s == nil { - return SlotUnAssigned + return SlotUnassigned } index, offset := s.convertIndex(i) return SlotAssignStatus((s.data[index] >> offset) & 3) @@ -438,9 +466,23 @@ func (s *Slots) IsSet(i int) bool { return (s.data[index]>>offset)&uint8(SlotAssigned) == uint8(SlotAssigned) } +func (s *Slots) IsImporting() bool { + if s == nil { + return false + } + return len(s.importingSlots) > 0 +} + +func (s *Slots) IsMigration() bool { + if s == nil { + return false + } + return len(s.migratingSlots) > 0 +} + func (s *Slots) MoveingStatus(i int) (SlotAssignStatus, string) { if s == nil { - return SlotUnAssigned, "" + return SlotUnassigned, "" } index, offset := s.convertIndex(i) status := SlotAssignStatus((s.data[index] >> offset) & 3) diff --git a/pkg/slot/slot_test.go b/pkg/slot/slot_test.go new file mode 100644 index 0000000..5a19b38 --- /dev/null +++ b/pkg/slot/slot_test.go @@ -0,0 +1,821 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package slot + +import ( + "reflect" + "testing" +) + +var ( + slotsA = &Slots{} + slotsB = &Slots{} + slotsC = &Slots{} + slotsD = &Slots{} + + slotsInterDB = &Slots{} + slotsInterDC = &Slots{} + allSlots *Slots +) + +func init() { + if err := slotsA.Set("0-5461", SlotAssigned); err != nil { + panic(err) + } + if err := slotsB.Set("5462-10922", SlotAssigned); err != nil { + panic(err) + } + if err := slotsC.Set("10923-16383", SlotAssigned); err != nil { + panic(err) + } + if err := slotsD.Set("0-5461,5464,10922,16000-16111", SlotAssigned); err != nil { + panic(err) + } + if err := slotsInterDB.Set("5464,10922", SlotAssigned); err != nil { + panic(err) + } + if err := slotsInterDC.Set("16000-16111", SlotAssigned); err != nil { + panic(err) + } + + allSlots = slotsA.Union(slotsB, slotsC) +} + +func TestNewSlotAssignStatusFromString(t *testing.T) { + tests := []struct { + name string + v string + want SlotAssignStatus + }{ + {"SlotImporting", "SlotImporting", SlotImporting}, + {"SlotAssigned", "SlotAssigned", SlotAssigned}, + {"SlotMigrating", "SlotMigrating", SlotMigrating}, + {"SlotUnassigned", "SlotUnassigned", SlotUnassigned}, + {"SlotUnassigned", "Unknown", SlotUnassigned}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewSlotAssignStatusFromString(tt.v) + if got != tt.want { + t.Errorf("NewSlotAssignStatusFromString() = %v, want %v", got, tt.want) + } + if got.String() != tt.name { + t.Errorf("SlotAssignStatus.String() = %v, want %v", got.String(), tt.name) + } + }) + } +} + +func TestSlots_IsFullfilled(t *testing.T) { + tests := []struct { + name string + s *Slots + wantFullfill bool + }{ + { + name: "nil", + s: nil, + wantFullfill: false, + }, + { + name: "slotsA", + s: slotsA, + wantFullfill: false, + }, + { + name: "slotsB", + s: slotsB, + wantFullfill: false, + }, + { + name: "slotsC", + s: slotsC, + wantFullfill: false, + }, + { + name: "allSlots", + s: allSlots, + wantFullfill: true, + }, + { + name: "slotsD", + s: slotsD, + wantFullfill: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.IsFullfilled(); got != tt.wantFullfill { + t.Errorf("Slots.IsFullfilled() = %v, want %v, %v", got, tt.wantFullfill, tt.s.Slots()) + } + }) + } +} + +func TestSlots_Inter(t *testing.T) { + type args struct { + n *Slots + } + tests := []struct { + name string + s *Slots + args args + want *Slots + }{ + { + name: "with_inter_d_b", + s: slotsD, + args: args{ + n: slotsB, + }, + want: slotsInterDB, + }, + { + name: "with_inter_d_c", + s: slotsD, + args: args{ + n: slotsC, + }, + want: slotsInterDC, + }, + { + name: "nil left", + s: nil, + args: args{ + n: slotsB, + }, + want: NewSlots(), + }, + { + name: "nil right", + s: slotsA, + args: args{ + n: nil, + }, + want: NewSlots(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Inter(tt.args.n); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Slots.Inter() = %v, want %v", got.Slots(), tt.want.Slots()) + } + }) + } +} + +func TestSlots_Equals(t *testing.T) { + slotsA := Slots{} + slotsB := Slots{} + slotsC := Slots{} + slotsD := Slots{} + for i := 0; i < 1000; i++ { + if i < 500 { + status := SlotAssigned + if i%2 == 0 { + status = SlotMigrating + } + _ = slotsA.Set(i, SlotAssignStatus(status)) + } else { + _ = slotsA.Set(i, SlotImporting) + } + } + _ = slotsB.Set("0-499", SlotAssigned) + _ = slotsC.Set("0-498", SlotAssigned) + _ = slotsC.Set("499", SlotImporting) + _ = slotsD.Set("0-498", SlotAssigned) + _ = slotsD.Set("499", SlotMigrating) + type args struct { + old *Slots + } + tests := []struct { + name string + s *Slots + args args + want bool + }{ + { + name: "equals", + s: &slotsA, + args: args{old: &slotsB}, + want: true, + }, + { + name: "nil left", + s: nil, + args: args{old: &slotsB}, + want: false, + }, + { + name: "nil right", + s: &slotsA, + args: args{old: nil}, + want: false, + }, + { + name: "nil left right", + s: nil, + args: args{old: nil}, + want: true, + }, + { + name: "importing", + s: &slotsA, + args: args{old: &slotsC}, + want: false, + }, + { + name: "migrating", + s: &slotsA, + args: args{old: &slotsD}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Equals(tt.args.old); got != tt.want { + t.Errorf("Slots.Equals() = %v, want %v %v %v", got, tt.want, tt.s.Slots(), tt.args.old.Slots()) + } + }) + } +} + +func TestSlots_Union(t *testing.T) { + type args struct { + slots []*Slots + } + tests := []struct { + name string + s *Slots + args args + want *Slots + }{ + { + name: "all", + s: slotsA, + args: args{slots: []*Slots{slotsB, slotsC}}, + want: allSlots, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Union(tt.args.slots...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Slots.Union() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlots_String(t *testing.T) { + slots := Slots{} + _ = slots.Set(0, SlotAssigned) + _ = slots.Set("1-100", SlotImporting) + _ = slots.Set("1000-2000", SlotMigrating) + _ = slots.Set("5000-10000", SlotAssigned) + _ = slots.Set("16111,16121,16131", SlotImporting) + _ = slots.Set("16112,16122,16132", SlotMigrating) + _ = slots.Set("16113,16123,16153", SlotAssigned) + _ = slots.Set("5201,5233,5400", SlotUnassigned) + + tests := []struct { + name string + s *Slots + wantRet string + }{ + { + name: "nil", + s: nil, + wantRet: "", + }, + { + name: "slots", + s: &slots, + wantRet: "0,1000-2000,5000-5200,5202-5232,5234-5399,5401-10000,16112-16113,16122-16123,16132,16153", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotRet := tt.s.String(); !reflect.DeepEqual(gotRet, tt.wantRet) { + t.Errorf("Slots.Slots() = %v, want %v", gotRet, tt.wantRet) + } + }) + } +} + +func TestSlots_Status(t *testing.T) { + slots := Slots{} + _ = slots.Set(0, SlotAssigned) + _ = slots.Set(100, SlotImporting) + _ = slots.Set(1, SlotMigrating) + _ = slots.Set(10000, SlotMigrating) + _ = slots.Set(10000, SlotUnassigned) + _ = slots.Set(11000, SlotImporting) + _ = slots.Set(11000, SlotAssigned) + type args struct { + i int + } + + tests := []struct { + name string + s *Slots + args args + want SlotAssignStatus + }{ + { + name: "nil", + s: nil, + args: args{i: 0}, + want: SlotUnassigned, + }, + { + name: "assigned", + s: &slots, + args: args{i: 0}, + want: SlotAssigned, + }, + { + name: "importing", + s: &slots, + args: args{i: 100}, + want: SlotImporting, + }, + { + name: "migrating", + s: &slots, + args: args{i: 1}, + want: SlotMigrating, + }, + { + name: "unassigned", + s: &slots, + args: args{i: 10000}, + want: SlotUnassigned, + }, + { + name: "importing=>assigined", + s: &slots, + args: args{i: 11000}, + want: SlotAssigned, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Status(tt.args.i); got != tt.want { + t.Errorf("Slots.Status() = %v, want %v, %v", got, tt.want, tt.s) + } + }) + } +} + +func TestSlots_Sub(t *testing.T) { + var ( + slotsA = &Slots{} + slotsB = &Slots{} + slotsC = &Slots{} + + slotsDiff = &Slots{} + ) + + _ = slotsA.Set("0-1000", SlotImporting) + _ = slotsA.Set("999-2000", SlotAssigned) + _ = slotsA.Set("2001-3000", SlotMigrating) + _ = slotsA.Set("5001-6000", SlotUnassigned) + + _ = slotsB.Set("0-1000", SlotAssigned) + _ = slotsB.Set("999-2000", SlotMigrating) + _ = slotsB.Set("2001-3000", SlotAssigned) + _ = slotsB.Set("5001-6000", SlotAssigned) + + _ = slotsC.Set("0-998", SlotMigrating) + _ = slotsC.Set("2000", SlotAssigned) + _ = slotsC.Set("2100", SlotAssigned) + _ = slotsC.Set("2101", SlotMigrating) + _ = slotsC.Set("2102", SlotImporting) + _ = slotsC.Set("2103", SlotUnassigned) + _ = slotsC.Set("5001", SlotAssigned) + _ = slotsC.Set("5002", SlotImporting) + _ = slotsC.Set("5003", SlotMigrating) + + _ = slotsDiff.Set("999-1999,2001-2099,2102-3000", SlotAssigned) + + type args struct { + n *Slots + } + tests := []struct { + name string + s *Slots + args args + want *Slots + }{ + { + name: "no sub", + s: slotsA, + args: args{n: slotsB}, + want: &Slots{}, + }, + { + name: "sub", + s: slotsA, + args: args{n: slotsC}, + want: slotsDiff, + }, + { + name: "nil right", + s: slotsA, + args: args{n: nil}, + want: slotsA, + }, + { + name: "nil left", + s: nil, + args: args{n: slotsA}, + want: NewSlots(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Sub(tt.args.n); !reflect.DeepEqual(got.data, tt.want.data) { + t.Errorf("Slots.Sub() = %v, want %v", got.Slots(), tt.want.Slots()) + } + }) + } +} + +func TestSlots_Load(t *testing.T) { + slots := NewSlots() + type args struct { + v interface{} + } + tests := []struct { + name string + slots *Slots + args args + wantErr bool + }{ + { + name: "nil", + slots: nil, + args: args{ + v: []string{"5000-5100", "[1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1]", "[77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca]"}, + }, + wantErr: false, + }, + { + name: "load", + slots: slots, + args: args{ + v: []string{"5000-5100", "[1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1]", "[77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca]"}, + }, + wantErr: false, + }, + { + name: "load more", + slots: slots, + args: args{ + v: []string{"5000-5100,10000-16666"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.slots.Load(tt.args.v); (err != nil) != tt.wantErr { + t.Errorf("Slots.Load() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSlots_Count(t *testing.T) { + slots := NewSlots() + if err := slots.Load([]string{ + "1-100", + "[1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1]", + "77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca]", + }); err != nil { + t.Fatal(err) + } + + type args struct { + status SlotAssignStatus + } + tests := []struct { + name string + slots *Slots + args args + wantC int + }{ + { + name: "nil", + slots: nil, + args: args{ + status: SlotAssigned, + }, + wantC: 0, + }, + { + name: "assigned", + slots: slots, + args: args{ + status: SlotAssigned, + }, + wantC: 100, + }, + { + name: "importing", + slots: slots, + args: args{ + status: SlotImporting, + }, + wantC: 1, + }, + { + name: "migrating", + slots: slots, + args: args{ + status: SlotMigrating, + }, + wantC: 1, + }, + { + name: "unassigned", + slots: slots, + args: args{ + status: SlotUnassigned, + }, + wantC: 16284, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotC := tt.slots.Count(tt.args.status); gotC != tt.wantC { + t.Errorf("Slots.Count() = %v, want %v. %v", gotC, tt.wantC, tt.slots.Slots()) + } + }) + } +} + +func TestLoadSlots(t *testing.T) { + tests := []struct { + name string + v any + wantErr bool + }{ + { + name: "number", + v: 100, + wantErr: false, + }, + { + name: "number2", + v: []int{100, 200}, + wantErr: false, + }, + { + name: "invalid number", + v: "xxxx", + wantErr: true, + }, + { + name: "number str", + v: "0-100", + wantErr: false, + }, + { + name: "invalid number str", + v: "xxx-yyy", + wantErr: true, + }, + { + name: "invalid number str", + v: "-", + wantErr: true, + }, + { + name: "not complete range", + v: "0-100,200-", + wantErr: true, + }, + { + name: "invalid range", + v: "200-100", + wantErr: true, + }, + { + name: "not complete migrate", + v: "77->-", + wantErr: true, + }, + { + name: "not complete migrate", + v: "->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "invalid slot migrate", + v: "xxx->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "not complete import", + v: "77-<-", + wantErr: true, + }, + { + name: "not complete import", + v: "-<-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "invalid slot import", + v: "xxx-<-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "error", + v: struct{}{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := LoadSlots(tt.v) + if (err != nil) != tt.wantErr { + t.Errorf("LoadSlots() error = %v, wantErr %v", err, false) + } + }) + } +} + +func TestSetSlots(t *testing.T) { + tests := []struct { + name string + v any + wantErr bool + }{ + { + name: "number", + v: 100, + wantErr: false, + }, + { + name: "number2", + v: []int{100, 200}, + wantErr: false, + }, + { + name: "invalid number", + v: "xxxx", + wantErr: true, + }, + { + name: "number str", + v: "0-100", + wantErr: false, + }, + { + name: "invalid number str", + v: "xxx-yyy", + wantErr: true, + }, + { + name: "invalid number str", + v: "-", + wantErr: true, + }, + { + name: "not complete range", + v: "0-100,200-", + wantErr: true, + }, + { + name: "invalid range", + v: "200-100", + wantErr: true, + }, + { + name: "string arr", + v: []string{"0-100", "200-300"}, + wantErr: false, + }, + { + name: "string arr", + v: []string{"0-100", "200-"}, + wantErr: true, + }, + { + name: "not complete migrate", + v: "77->-", + wantErr: true, + }, + { + name: "not complete migrate", + v: "->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "invalid slot migrate", + v: "xxx->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "not complete import", + v: "77-<-", + wantErr: true, + }, + { + name: "not complete import", + v: "-<-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "invalid slot import", + v: "xxx-<-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca", + wantErr: true, + }, + { + name: "error", + v: struct{}{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + slot := NewSlots() + err := slot.Set(tt.v, SlotAssigned) + if (err != nil) != tt.wantErr { + t.Errorf("SetSlots() error = %v, wantErr %v", err, false) + } + }) + } +} + +func TestNewFullSlots(t *testing.T) { + slots := NewFullSlots() + if !slots.IsFullfilled() { + t.Errorf("NewFullSlots() = not fullfilled, want fullfilled") + } + if !IsFullfilled(slots) { + t.Errorf("IsFullfilled() = fullfilled, want not fullfilled") + } +} + +func TestSlots_SlotsByStatus(t *testing.T) { + slots := NewSlots() + _ = slots.Set("0-100", SlotAssigned) + if len(slots.SlotsByStatus(SlotAssigned)) != 101 { + t.Errorf("Slots.SlotsByStatus() = %v, want %v", len(slots.SlotsByStatus(SlotAssigned)), 101) + } + if val := (*Slots)(nil).SlotsByStatus(SlotAssigned); val != nil { + t.Errorf("Slots.SlotsByStatus() = %v, want %v", val, nil) + } +} + +func TestSlots_IsSet(t *testing.T) { + slots := NewSlots() + _ = slots.Set("0-100", SlotAssigned) + if !slots.IsSet(50) { + t.Errorf("Slots.IsSet() = false, want true") + } + if (*Slots)(nil).IsSet(0) { + t.Errorf("Slots.IsSet() = true, want false") + } +} + +func TestSlots_MoveingStatus(t *testing.T) { + slots := NewSlots() + _ = slots.Set("0-100", SlotImporting) + _ = slots.Set("200-300", SlotMigrating) + status, _ := slots.MoveingStatus(50) + if status != SlotImporting { + t.Errorf("Slots.MoveingStatus() = %v, want %v", status, SlotImporting) + } + status, _ = slots.MoveingStatus(201) + if status != SlotMigrating { + t.Errorf("Slots.MoveingStatus() = %v, want %v", status, SlotMigrating) + } + status, _ = slots.MoveingStatus(401) + if status != SlotUnassigned { + t.Errorf("Slots.MoveingStatus() = %v, want %v", status, SlotUnassigned) + } + if status, _ := (*Slots)(nil).MoveingStatus(0); status != SlotUnassigned { + t.Errorf("Slots.MoveingStatus() = %v, want %v", status, SlotUnassigned) + } +} diff --git a/pkg/types/cluster.go b/pkg/types/cluster.go deleted file mode 100644 index 474f4ea..0000000 --- a/pkg/types/cluster.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package types - -import ( - "context" - - clusterv1 "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - - "github.com/alauda/redis-operator/pkg/security/acl" - "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/alauda/redis-operator/pkg/types/slot" - appv1 "k8s.io/api/apps/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// RedisClusterShard -type RedisClusterShard interface { - v1.Object - GetObjectKind() schema.ObjectKind - Definition() *appv1.StatefulSet - Status() *appv1.StatefulSetStatus - - // Version return the current version of redis image - Version() redis.RedisVersion - - Index() int - Nodes() []redis.RedisNode - // Master returns the master node of this shard which has joined the cluster - // Keep in mind that, this not means the master has been assigned slots - Master() redis.RedisNode - // Replicas returns nodes whoses role is slave - Replicas() []redis.RedisNode - - Slots() *slot.Slots - IsImporting() bool - IsMigrating() bool - - Restart(ctx context.Context) error - Refresh(ctx context.Context) error -} - -// RedisInstance -type RedisClusterInstance interface { - RedisInstance - Status() *clusterv1.DistributedRedisClusterStatus - Definition() *clusterv1.DistributedRedisCluster - Shards() []RedisClusterShard - UpdateStatus(ctx context.Context, status clusterv1.ClusterStatus, message string, shards []*clusterv1.ClusterShards) error -} - -type RedisInstance interface { - v1.Object - Users() acl.Users - GetObjectKind() schema.ObjectKind - Version() redis.RedisVersion - Masters() []redis.RedisNode - Nodes() []redis.RedisNode - IsInService() bool - IsReady() bool - Restart(ctx context.Context) error - Refresh(ctx context.Context) error - IsACLUserExists() bool -} diff --git a/pkg/types/instance.go b/pkg/types/instance.go new file mode 100644 index 0000000..e19019c --- /dev/null +++ b/pkg/types/instance.go @@ -0,0 +1,178 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "context" + "crypto/tls" + + clusterv1 "github.com/alauda/redis-operator/api/cluster/v1alpha1" + databasesv1 "github.com/alauda/redis-operator/api/databases/v1" + "github.com/alauda/redis-operator/pkg/redis" + "github.com/alauda/redis-operator/pkg/security/acl" + "github.com/alauda/redis-operator/pkg/slot" + rtypes "github.com/alauda/redis-operator/pkg/types/redis" + "github.com/go-logr/logr" + appv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Object interface { + v1.Object + GetObjectKind() schema.ObjectKind + DeepCopyObject() runtime.Object + NamespacedName() client.ObjectKey + Version() rtypes.RedisVersion + IsReady() bool + + Restart(ctx context.Context, annotationKeyVal ...string) error + Refresh(ctx context.Context) error +} + +type InstanceStatus string + +const ( + Any InstanceStatus = "" + OK InstanceStatus = "OK" + Fail InstanceStatus = "Fail" + Paused InstanceStatus = "Paused" +) + +type RedisInstance interface { + Object + + Arch() rtypes.RedisArch + Users() acl.Users + TLSConfig() *tls.Config + IsInService() bool + IsACLUserExists() bool + IsACLAppliedToAll() bool + IsResourceFullfilled(ctx context.Context) (bool, error) + UpdateStatus(ctx context.Context, st InstanceStatus, message string) error + SendEventf(eventtype, reason, messageFmt string, args ...interface{}) + Logger() logr.Logger +} + +type RedisReplication interface { + Object + + Definition() *appv1.StatefulSet + Status() *appv1.StatefulSetStatus + + // Master returns the master node of this shard which has joined the cluster + // Keep in mind that, this not means the master has been assigned slots + Master() rtypes.RedisNode + // Replicas returns nodes whoses role is slave + Replicas() []rtypes.RedisNode + Nodes() []rtypes.RedisNode +} + +type RedisSentinel interface { + Object + + Definition() *appv1.Deployment + Status() *appv1.DeploymentStatus + + Nodes() []rtypes.RedisSentinelNode +} + +type RedisSentinelReplication interface { + Object + + Definition() *appv1.StatefulSet + Status() *appv1.StatefulSetStatus + + Nodes() []rtypes.RedisSentinelNode +} + +type RedisSentinelInstance interface { + RedisInstance + + Definition() *databasesv1.RedisSentinel + Replication() RedisSentinelReplication + Nodes() []rtypes.RedisSentinelNode + RawNodes(ctx context.Context) ([]corev1.Pod, error) + + // helper methods + GetPassword() (string, error) + + Selector() map[string]string +} + +type FailoverMonitor interface { + Policy() databasesv1.FailoverPolicy + Master(ctx context.Context) (*redis.SentinelMonitorNode, error) + Replicas(ctx context.Context) ([]*redis.SentinelMonitorNode, error) + Inited(ctx context.Context) (bool, error) + AllNodeMonitored(ctx context.Context) (bool, error) + UpdateConfig(ctx context.Context, params map[string]string) error + Failover(ctx context.Context) error + Monitor(ctx context.Context, node rtypes.RedisNode) error +} + +type RedisFailoverInstance interface { + RedisInstance + + Definition() *databasesv1.RedisFailover + Masters() []rtypes.RedisNode + Nodes() []rtypes.RedisNode + RawNodes(ctx context.Context) ([]corev1.Pod, error) + Monitor() FailoverMonitor + + IsBindedSentinel() bool + IsStandalone() bool + Selector() map[string]string +} + +// RedisClusterShard +type RedisClusterShard interface { + Object + + Definition() *appv1.StatefulSet + Status() *appv1.StatefulSetStatus + + Index() int + Nodes() []rtypes.RedisNode + // Master returns the master node of this shard which has joined the cluster + // Keep in mind that, this not means the master has been assigned slots + Master() rtypes.RedisNode + // Replicas returns nodes whoses role is slave + Replicas() []rtypes.RedisNode + + // Slots returns the slots of this shard + Slots() *slot.Slots + IsImporting() bool + IsMigrating() bool +} + +// RedisInstance +type RedisClusterInstance interface { + RedisInstance + + Definition() *clusterv1.DistributedRedisCluster + Status() *clusterv1.DistributedRedisClusterStatus + + Masters() []rtypes.RedisNode + Nodes() []rtypes.RedisNode + RawNodes(ctx context.Context) ([]corev1.Pod, error) + Shards() []RedisClusterShard + RewriteShards(ctx context.Context, shards []*clusterv1.ClusterShards) error +} diff --git a/pkg/types/redis/redis.go b/pkg/types/redis/redis.go index 358f578..b359853 100644 --- a/pkg/types/redis/redis.go +++ b/pkg/types/redis/redis.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -21,49 +21,26 @@ import ( "net" "strings" + "github.com/alauda/redis-operator/api/core" rediscli "github.com/alauda/redis-operator/pkg/redis" - "github.com/alauda/redis-operator/pkg/types/slot" + "github.com/alauda/redis-operator/pkg/slot" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) -type RedisArch string +type RedisArch = core.Arch -const ( - StandaloneArch RedisArch = "standalone" - SentinelArch RedisArch = "sentinel" - ClusterArch RedisArch = "cluster" -) - -type SentinelRole string - -const ( - SentinelRoleMaster SentinelRole = "master" - SentinelRoleSlave SentinelRole = "slave" - SentinelRoleSentinel SentinelRole = "sentinel" -) - -// RedisRole redis node role type -type RedisRole string - -const ( - // RedisRoleMaster RedisCluster Master node role - RedisRoleMaster RedisRole = "Master" - // RedisRoleSlave RedisCluster Master node role - RedisRoleSlave RedisRole = "Slave" - // RedisNodeRoleNone None node role - RedisRoleNone RedisRole = "None" -) - -func NewRedisRole(v string) RedisRole { +func NewRedisRole(v string) core.RedisRole { switch strings.ToLower(v) { case "master": - return RedisRoleMaster + return core.RedisRoleMaster case "slave", "replica": - return RedisRoleSlave + return core.RedisRoleReplica + case "sentinel": + return core.RedisRoleSentinel } - return RedisRoleNone + return core.RedisRoleNone } // RedisNode @@ -101,22 +78,21 @@ type RedisNode interface { // Role returns the role of current node // be sure that for the new start redis server, the role is master when in cluster mode - Role() RedisRole + Role() core.RedisRole // Slots if this node is master, returns the slots this nodes assigned // else returns nil Slots() *slot.Slots - GetPod() *corev1.Pod Config() map[string]string ConfigedMasterIP() string ConfigedMasterPort() string // Setup Setup(ctx context.Context, margs ...[]any) error - SetMonitor(ctx context.Context, ip, port, user, password, quorum string) error ReplicaOf(ctx context.Context, ip, port string) error SetACLUser(ctx context.Context, username string, passwords []string, rules string) (interface{}, error) Query(ctx context.Context, cmd string, args ...any) (any, error) Info() rediscli.RedisInfo + ClusterInfo() rediscli.RedisClusterInfo IPort() int InternalIPort() int @@ -132,3 +108,51 @@ type RedisNode interface { Refresh(ctx context.Context) error } + +// RedisSentinelNode +type RedisSentinelNode interface { + metav1.Object + RedisSentinelNodeOperation + GetObjectKind() schema.ObjectKind + + Definition() *corev1.Pod + + // Index the index of statefulset + Index() int + // IsTerminating indicate whether is pod is deleted + IsTerminating() bool + // IsReady indicate whether is main container is ready + IsReady() bool + // IsACLApplied returns true when the main container got ACL_CONFIGMAP_NAME env + IsACLApplied() bool + + Port() int + InternalPort() int + DefaultIP() net.IP + DefaultInternalIP() net.IP + IPs() []net.IP + NodeIP() net.IP + + Status() corev1.PodPhase + ContainerStatus() *corev1.ContainerStatus +} + +type RedisSentinelNodeOperation interface { + // CurrentVersion return current redis server version + // this value maybe differ with cr def when do version upgrade + CurrentVersion() RedisVersion + + Refresh(ctx context.Context) error + + Config() map[string]string + + // Setup + Setup(ctx context.Context, margs ...[]any) error + SetMonitor(ctx context.Context, name, ip, port, user, password, quorum string) error + Query(ctx context.Context, cmd string, args ...any) (any, error) + Info() rediscli.RedisInfo + // sentinel inspect + Brothers(ctx context.Context, name string) ([]*rediscli.SentinelMonitorNode, error) + MonitoringClusters(ctx context.Context) (clusters []string, err error) + MonitoringNodes(ctx context.Context, name string) (master *rediscli.SentinelMonitorNode, replicas []*rediscli.SentinelMonitorNode, err error) +} diff --git a/pkg/types/redis/redis_test.go b/pkg/types/redis/redis_test.go new file mode 100644 index 0000000..cdf71d9 --- /dev/null +++ b/pkg/types/redis/redis_test.go @@ -0,0 +1,165 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package redis + +import ( + "context" + "net" + "testing" + + "github.com/alauda/redis-operator/api/core" + rediscli "github.com/alauda/redis-operator/pkg/redis" + "github.com/alauda/redis-operator/pkg/slot" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestNewRedisRole(t *testing.T) { + testCases := []struct { + input string + expected core.RedisRole + }{ + {"master", core.RedisRoleMaster}, + {"slave", core.RedisRoleReplica}, + {"replica", core.RedisRoleReplica}, + {"sentinel", core.RedisRoleSentinel}, + {"unknown", core.RedisRoleNone}, + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + result := NewRedisRole(tc.input) + if result != tc.expected { + t.Errorf("Expected %v, got %v", tc.expected, result) + } + }) + } +} + +// Mock implementations for RedisNode and RedisSentinelNode interfaces +type MockRedisNode struct{} + +func (m *MockRedisNode) GetObjectKind() schema.ObjectKind { return nil } +func (m *MockRedisNode) Definition() *corev1.Pod { return nil } +func (m *MockRedisNode) ID() string { return "mock-id" } +func (m *MockRedisNode) Index() int { return 0 } +func (m *MockRedisNode) IsConnected() bool { return true } +func (m *MockRedisNode) IsTerminating() bool { return false } +func (m *MockRedisNode) IsMasterLinkUp() bool { return true } +func (m *MockRedisNode) IsReady() bool { return true } +func (m *MockRedisNode) IsJoined() bool { return true } +func (m *MockRedisNode) MasterID() string { return "master-id" } +func (m *MockRedisNode) IsMasterFailed() bool { return false } +func (m *MockRedisNode) CurrentVersion() RedisVersion { return "6.2.1" } +func (m *MockRedisNode) IsACLApplied() bool { return true } +func (m *MockRedisNode) Role() core.RedisRole { return core.RedisRoleMaster } +func (m *MockRedisNode) Slots() *slot.Slots { return nil } +func (m *MockRedisNode) Config() map[string]string { return nil } +func (m *MockRedisNode) ConfigedMasterIP() string { return "127.0.0.1" } +func (m *MockRedisNode) ConfigedMasterPort() string { return "6379" } +func (m *MockRedisNode) Setup(ctx context.Context, margs ...[]any) error { return nil } +func (m *MockRedisNode) ReplicaOf(ctx context.Context, ip, port string) error { return nil } +func (m *MockRedisNode) SetACLUser(ctx context.Context, username string, passwords []string, rules string) (interface{}, error) { + return nil, nil +} +func (m *MockRedisNode) Query(ctx context.Context, cmd string, args ...any) (any, error) { + return nil, nil +} +func (m *MockRedisNode) Info() rediscli.RedisInfo { return rediscli.RedisInfo{} } +func (m *MockRedisNode) ClusterInfo() rediscli.RedisClusterInfo { return rediscli.RedisClusterInfo{} } +func (m *MockRedisNode) IPort() int { return 6379 } +func (m *MockRedisNode) InternalIPort() int { return 6379 } +func (m *MockRedisNode) Port() int { return 6379 } +func (m *MockRedisNode) InternalPort() int { return 6379 } +func (m *MockRedisNode) DefaultIP() net.IP { return net.ParseIP("127.0.0.1") } +func (m *MockRedisNode) DefaultInternalIP() net.IP { return net.ParseIP("127.0.0.1") } +func (m *MockRedisNode) IPs() []net.IP { return []net.IP{net.ParseIP("127.0.0.1")} } +func (m *MockRedisNode) NodeIP() net.IP { return net.ParseIP("127.0.0.1") } +func (m *MockRedisNode) Status() corev1.PodPhase { return corev1.PodRunning } +func (m *MockRedisNode) ContainerStatus() *corev1.ContainerStatus { return nil } +func (m *MockRedisNode) Refresh(ctx context.Context) error { return nil } + +func TestRedisNodeMethods(t *testing.T) { + node := &MockRedisNode{} + + if node.ID() != "mock-id" { + t.Errorf("Expected ID 'mock-id', got %s", node.ID()) + } + if !node.IsConnected() { + t.Errorf("Expected IsConnected to be true") + } + if node.IsTerminating() { + t.Errorf("Expected IsTerminating to be false") + } + if !node.IsMasterLinkUp() { + t.Errorf("Expected IsMasterLinkUp to be true") + } + if !node.IsReady() { + t.Errorf("Expected IsReady to be true") + } + if !node.IsJoined() { + t.Errorf("Expected IsJoined to be true") + } + if node.MasterID() != "master-id" { + t.Errorf("Expected MasterID 'master-id', got %s", node.MasterID()) + } + if node.IsMasterFailed() { + t.Errorf("Expected IsMasterFailed to be false") + } + if node.CurrentVersion() != "6.2.1" { + t.Errorf("Expected CurrentVersion '6.2.1', got %s", node.CurrentVersion()) + } + if !node.IsACLApplied() { + t.Errorf("Expected IsACLApplied to be true") + } + if node.Role() != core.RedisRoleMaster { + t.Errorf("Expected Role 'master', got %v", node.Role()) + } + if node.ConfigedMasterIP() != "127.0.0.1" { + t.Errorf("Expected ConfigedMasterIP '127.0.0.1', got %s", node.ConfigedMasterIP()) + } + if node.ConfigedMasterPort() != "6379" { + t.Errorf("Expected ConfigedMasterPort '6379', got %s", node.ConfigedMasterPort()) + } + if node.IPort() != 6379 { + t.Errorf("Expected IPort 6379, got %d", node.IPort()) + } + if node.InternalIPort() != 6379 { + t.Errorf("Expected InternalIPort 6379, got %d", node.InternalIPort()) + } + if node.Port() != 6379 { + t.Errorf("Expected Port 6379, got %d", node.Port()) + } + if node.InternalPort() != 6379 { + t.Errorf("Expected InternalPort 6379, got %d", node.InternalPort()) + } + if !node.DefaultIP().Equal(net.ParseIP("127.0.0.1")) { + t.Errorf("Expected DefaultIP '127.0.0.1', got %s", node.DefaultIP()) + } + if !node.DefaultInternalIP().Equal(net.ParseIP("127.0.0.1")) { + t.Errorf("Expected DefaultInternalIP '127.0.0.1', got %s", node.DefaultInternalIP()) + } + if len(node.IPs()) != 1 || !node.IPs()[0].Equal(net.ParseIP("127.0.0.1")) { + t.Errorf("Expected IPs '[127.0.0.1]', got %v", node.IPs()) + } + if !node.NodeIP().Equal(net.ParseIP("127.0.0.1")) { + t.Errorf("Expected NodeIP '127.0.0.1', got %s", node.NodeIP()) + } + if node.Status() != corev1.PodRunning { + t.Errorf("Expected Status 'Running', got %v", node.Status()) + } +} diff --git a/pkg/types/redis/version.go b/pkg/types/redis/version.go index 1e112d1..5ff1c2d 100644 --- a/pkg/types/redis/version.go +++ b/pkg/types/redis/version.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -22,6 +22,7 @@ import ( "strings" "github.com/Masterminds/semver/v3" + "github.com/alauda/redis-operator/api/core" ) var ( @@ -30,8 +31,10 @@ var ( MinACL2SupportedVersion, _ = semver.NewVersion("7.0-AAA") _Redis5Version, _ = semver.NewVersion("5.0-AAA") - // _Redis6Version, _ = semver.NewVersion("6.0-AAA") + _Redis6Version, _ = semver.NewVersion("6.0-AAA") _Redis7Version, _ = semver.NewVersion("7.0-AAA") + + _ = _Redis6Version ) // RedisVersion @@ -41,22 +44,25 @@ const ( // RedisVersionUnknown RedisVersionUnknown RedisVersion = "" // RedisVersion4 - RedisVersion4 = "4.0" + RedisVersion4 RedisVersion = "4.0" // RedisVersion5 - RedisVersion5 = "5.0" - // RedisVersion6 - // - // Supports ACL, io-threads - RedisVersion6 = "6.0" + RedisVersion5 RedisVersion = "5.0" + // RedisVersion6, Supports ACL, io-threads + RedisVersion6 RedisVersion = "6.0" // RedisVersion6_2 - RedisVersion6_2 = "6.2" - - // Supports ACL2, Function + RedisVersion6_2 RedisVersion = "6.2" // RedisVersion7 - RedisVersion7 = "7.0" - RedisVersion7_2 = "7.2" + RedisVersion7 RedisVersion = "7.0" + // Supports ACL2, Function + RedisVersion7_2 RedisVersion = "7.2" + // Supports modules + ActiveRedisVersion6 RedisVersion = "6.0" ) +func (v RedisVersion) String() string { + return string(v) +} + func (v RedisVersion) IsTLSSupported() bool { ver, err := semver.NewVersion(string(v)) if err != nil { @@ -81,6 +87,14 @@ func (v RedisVersion) IsACL2Supported() bool { return ver.Compare(MinACL2SupportedVersion) >= 0 } +func (v RedisVersion) IsClusterShardSupported() bool { + ver, err := semver.NewVersion(string(v)) + if err != nil { + return false + } + return ver.Compare(_Redis7Version) >= 0 +} + func (v RedisVersion) CustomConfigs(arch RedisArch) map[string]string { if v == RedisVersionUnknown { return nil @@ -95,7 +109,7 @@ func (v RedisVersion) CustomConfigs(arch RedisArch) map[string]string { if ver.Compare(_Redis5Version) >= 0 { ret["ignore-warnings"] = "ARM64-COW-BUG" } - if arch == ClusterArch { + if arch == core.RedisCluster { switch { case ver.Compare(_Redis7Version) >= 0: ret["cluster-allow-replica-migration"] = "no" diff --git a/pkg/types/redis/version_test.go b/pkg/types/redis/version_test.go index 674d844..62f4f12 100644 --- a/pkg/types/redis/version_test.go +++ b/pkg/types/redis/version_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,7 +17,10 @@ limitations under the License. package redis import ( + "reflect" "testing" + + "github.com/alauda/redis-operator/api/core" ) func TestParseRedisVersion(t *testing.T) { @@ -36,6 +39,12 @@ func TestParseRedisVersion(t *testing.T) { want: RedisVersion6, wantErr: false, }, + { + name: "patch invalid version", + args: args{v: "abcdefg"}, + want: "", + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -44,9 +53,15 @@ func TestParseRedisVersion(t *testing.T) { t.Errorf("ParseRedisVersion() error = %v, wantErr %v", err, tt.wantErr) return } + if err != nil { + return + } if got != tt.want { t.Errorf("ParseRedisVersion() = %v, want %v", got, tt.want) } + if got.String() != "6.0" { + t.Errorf("ParseRedisVersion().String() = %v, want %v", got, tt.want) + } }) } } @@ -85,6 +100,18 @@ func TestParseRedisVersionFromImage(t *testing.T) { want: RedisVersion5, wantErr: false, }, + { + name: "invalid image", + args: args{u: "build-harbor.alauda.cn/middleware/redis5.0-alpine.000b26a0c3b6"}, + want: "", + wantErr: true, + }, + { + name: "latest", + args: args{u: "build-harbor.alauda.cn/middleware/redis:latest"}, + want: RedisVersion6, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -99,3 +126,193 @@ func TestParseRedisVersionFromImage(t *testing.T) { }) } } + +func TestIsTLSSupported(t *testing.T) { + tests := []struct { + name string + v RedisVersion + want bool + }{ + { + name: "TLS supported 7.2", + v: RedisVersion7_2, + want: true, + }, + { + name: "TLS supported 7.0", + v: RedisVersion7, + want: true, + }, + { + name: "TLS supported 6.0", + v: RedisVersion6, + want: true, + }, + { + name: "TLS not supported 5.0", + v: RedisVersion5, + want: false, + }, + { + name: "TLS not supported 4.0", + v: RedisVersion4, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.v.IsTLSSupported(); got != tt.want { + t.Errorf("IsTLSSupported() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsACLSupported(t *testing.T) { + tests := []struct { + name string + v RedisVersion + want bool + }{ + { + name: "ACL supported 7.2", + v: RedisVersion7_2, + want: true, + }, + { + name: "ACL supported 7.0", + v: RedisVersion7, + want: true, + }, + { + name: "ACL supported 6.0", + v: RedisVersion6, + want: true, + }, + { + name: "ACL not supported 5.0", + v: RedisVersion5, + want: false, + }, + { + name: "ACL not supported 4.0", + v: RedisVersion4, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.v.IsACLSupported(); got != tt.want { + t.Errorf("IsACLSupported() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsACL2Supported(t *testing.T) { + tests := []struct { + name string + v RedisVersion + want bool + }{ + { + name: "ACL2 supported 7.2", + v: RedisVersion7_2, + want: true, + }, + { + name: "ACL2 supported 7.0", + v: RedisVersion7, + want: true, + }, + { + name: "ACL2 not supported 6.0", + v: RedisVersion6, + want: false, + }, + { + name: "ACL2 not supported 5.0", + v: RedisVersion5, + want: false, + }, + { + name: "ACL2 not supported 4.0", + v: RedisVersion4, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.v.IsACL2Supported(); got != tt.want { + t.Errorf("IsACL2Supported() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsClusterShardSupported(t *testing.T) { + tests := []struct { + name string + v RedisVersion + want bool + }{ + { + name: "Cluster shard supported 7.2", + v: RedisVersion7_2, + want: true, + }, + { + name: "Cluster shard supported 7.0", + v: RedisVersion7, + want: true, + }, + { + name: "Cluster shard not supported 6.0", + v: RedisVersion6, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.v.IsClusterShardSupported(); got != tt.want { + t.Errorf("IsClusterShardSupported() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCustomConfigs(t *testing.T) { + tests := []struct { + name string + v RedisVersion + arch RedisArch + want map[string]string + }{ + { + name: "Redis 5 with ARM64", + v: RedisVersion5, + arch: core.RedisCluster, + want: map[string]string{ + "ignore-warnings": "ARM64-COW-BUG", + "cluster-migration-barrier": "10", + }, + }, + { + name: "Redis 7 with ARM64", + v: RedisVersion7, + arch: core.RedisCluster, + want: map[string]string{ + "ignore-warnings": "ARM64-COW-BUG", + "cluster-allow-replica-migration": "no", + "cluster-migration-barrier": "10", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.v.CustomConfigs(tt.arch); !reflect.DeepEqual(got, tt.want) { + t.Errorf("CustomConfigs() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/types/sentinel.go b/pkg/types/sentinel.go deleted file mode 100644 index 4ad92e0..0000000 --- a/pkg/types/sentinel.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package types - -import ( - "context" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/pkg/types/redis" - appv1 "k8s.io/api/apps/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -type RedisSentinelReplica interface { - v1.Object - GetObjectKind() schema.ObjectKind - Definition() *appv1.StatefulSet - Status() *appv1.StatefulSetStatus - - // Version return the current version of redis image - Version() redis.RedisVersion - - Nodes() []redis.RedisNode - // Master returns the master node of this shard which has joined the cluster - // Keep in mind that, this not means the master has been assigned slots - Master() redis.RedisNode - // Replicas returns nodes whoses role is slave - Replicas() []redis.RedisNode - - Restart(ctx context.Context) error - Refresh(ctx context.Context) error -} - -type RedisSentinelNodes interface { - v1.Object - GetObjectKind() schema.ObjectKind - Definition() *appv1.Deployment - Status() *appv1.DeploymentStatus - // Version return the current version of redis image - Version() redis.RedisVersion - - Nodes() []redis.RedisNode - - Restart(ctx context.Context) error - Refresh(ctx context.Context) error -} - -type RedisFailoverInstance interface { - RedisInstance - Definition() *databasesv1.RedisFailover - SentinelNodes() []redis.RedisNode - Sentinel() RedisSentinelNodes - UpdateStatus(ctx context.Context, status databasesv1.RedisFailoverStatus) error - Selector() map[string]string -} diff --git a/pkg/types/slot/slot_test.go b/pkg/types/slot/slot_test.go deleted file mode 100644 index 6ede45f..0000000 --- a/pkg/types/slot/slot_test.go +++ /dev/null @@ -1,445 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package slot - -import ( - "reflect" - "testing" -) - -var ( - slotsA = &Slots{} - slotsB = &Slots{} - slotsC = &Slots{} - slotsD = &Slots{} - - slotsInterDB = &Slots{} - slotsInterDC = &Slots{} - allSlots *Slots -) - -func init() { - if err := slotsA.Set("0-5461", SlotAssigned); err != nil { - panic(err) - } - if err := slotsB.Set("5462-10922", SlotAssigned); err != nil { - panic(err) - } - if err := slotsC.Set("10923-16383", SlotAssigned); err != nil { - panic(err) - } - if err := slotsD.Set("0-5461,5464,10922,16000-16111", SlotAssigned); err != nil { - panic(err) - } - if err := slotsInterDB.Set("5464,10922", SlotAssigned); err != nil { - panic(err) - } - if err := slotsInterDC.Set("16000-16111", SlotAssigned); err != nil { - panic(err) - } - - allSlots = slotsA.Union(slotsB, slotsC) -} - -func TestSlots_IsFullfilled(t *testing.T) { - tests := []struct { - name string - s *Slots - wantFullfill bool - }{ - { - name: "slotsA", - s: slotsA, - wantFullfill: false, - }, - { - name: "slotsB", - s: slotsB, - wantFullfill: false, - }, - { - name: "slotsC", - s: slotsC, - wantFullfill: false, - }, - { - name: "allSlots", - s: allSlots, - wantFullfill: true, - }, - { - name: "slotsD", - s: slotsD, - wantFullfill: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.IsFullfilled(); got != tt.wantFullfill { - t.Errorf("Slots.IsFullfilled() = %v, want %v, %v", got, tt.wantFullfill, tt.s.Slots()) - } - }) - } -} - -func TestSlots_Inter(t *testing.T) { - type args struct { - n *Slots - } - tests := []struct { - name string - s *Slots - args args - want *Slots - }{ - { - name: "with_inter_d_b", - s: slotsD, - args: args{ - n: slotsB, - }, - want: slotsInterDB, - }, - { - name: "with_inter_d_c", - s: slotsD, - args: args{ - n: slotsC, - }, - want: slotsInterDC, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Inter(tt.args.n); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Slots.Inter() = %v, want %v", got.Slots(), tt.want.Slots()) - } - }) - } -} - -func TestSlots_Equals(t *testing.T) { - slotsA := Slots{} - slotsB := Slots{} - for i := 0; i < 1000; i++ { - if i < 500 { - status := SlotAssigned - if i%2 == 0 { - status = SlotMigrating - } - slotsA.Set(i, SlotAssignStatus(status)) - } else { - slotsA.Set(i, SlotImporting) - } - } - slotsB.Set("0-499", SlotAssigned) - type args struct { - old *Slots - } - tests := []struct { - name string - s *Slots - args args - want bool - }{ - { - name: "equals", - s: &slotsA, - args: args{old: &slotsB}, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Equals(tt.args.old); got != tt.want { - t.Errorf("Slots.Equals() = %v, want %v %v %v", got, tt.want, tt.s.Slots(), tt.args.old.Slots()) - } - }) - } -} - -func TestSlots_Union(t *testing.T) { - type args struct { - slots []*Slots - } - tests := []struct { - name string - s *Slots - args args - want *Slots - }{ - { - name: "all", - s: slotsA, - args: args{slots: []*Slots{slotsB, slotsC}}, - want: allSlots, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Union(tt.args.slots...); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Slots.Union() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestSlots_String(t *testing.T) { - slots := Slots{} - slots.Set(0, SlotAssigned) - slots.Set("1-100", SlotImporting) - slots.Set("1000-2000", SlotMigrating) - slots.Set("5000-10000", SlotAssigned) - slots.Set("16111,16121,16131", SlotImporting) - slots.Set("16112,16122,16132", SlotMigrating) - slots.Set("16113,16123,16153", SlotAssigned) - slots.Set("5201,5233,5400", SlotUnAssigned) - - tests := []struct { - name string - s *Slots - wantRet string - }{ - { - name: "slots", - s: &slots, - wantRet: "0,1000-2000,5000-5200,5202-5232,5234-5399,5401-10000,16112-16113,16122-16123,16132,16153", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotRet := tt.s.String(); !reflect.DeepEqual(gotRet, tt.wantRet) { - t.Errorf("Slots.Slots() = %v, want %v", gotRet, tt.wantRet) - } - }) - } -} - -func TestSlots_Status(t *testing.T) { - slots := Slots{} - slots.Set(0, SlotAssigned) - slots.Set(100, SlotImporting) - slots.Set(1, SlotMigrating) - slots.Set(10000, SlotMigrating) - slots.Set(10000, SlotUnAssigned) - slots.Set(11000, SlotImporting) - slots.Set(11000, SlotAssigned) - type args struct { - i int - } - - tests := []struct { - name string - s *Slots - args args - want SlotAssignStatus - }{ - { - name: "assigned", - s: &slots, - args: args{i: 0}, - want: SlotAssigned, - }, - { - name: "importing", - s: &slots, - args: args{i: 100}, - want: SlotImporting, - }, - { - name: "migrating", - s: &slots, - args: args{i: 1}, - want: SlotMigrating, - }, - { - name: "unassigned", - s: &slots, - args: args{i: 10000}, - want: SlotUnAssigned, - }, - { - name: "importing=>assigined", - s: &slots, - args: args{i: 11000}, - want: SlotAssigned, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Status(tt.args.i); got != tt.want { - t.Errorf("Slots.Status() = %v, want %v, %v", got, tt.want, tt.s) - } - }) - } -} - -func TestSlots_Sub(t *testing.T) { - var ( - slotsA = &Slots{} - slotsB = &Slots{} - slotsC = &Slots{} - - slotsDiff = &Slots{} - ) - - slotsA.Set("0-1000", SlotImporting) - slotsA.Set("999-2000", SlotAssigned) - slotsA.Set("2001-3000", SlotMigrating) - slotsA.Set("5001-6000", SlotUnAssigned) - - slotsB.Set("0-1000", SlotAssigned) - slotsB.Set("999-2000", SlotMigrating) - slotsB.Set("2001-3000", SlotAssigned) - slotsB.Set("5001-6000", SlotAssigned) - - slotsC.Set("0-998", SlotMigrating) - slotsC.Set("2000", SlotAssigned) - slotsC.Set("2100", SlotAssigned) - slotsC.Set("2101", SlotMigrating) - slotsC.Set("2102", SlotImporting) - slotsC.Set("2103", SlotUnAssigned) - slotsC.Set("5001", SlotAssigned) - slotsC.Set("5002", SlotImporting) - slotsC.Set("5003", SlotMigrating) - - slotsDiff.Set("999-1999,2001-2099,2102-3000", SlotAssigned) - - type args struct { - n *Slots - } - tests := []struct { - name string - s *Slots - args args - want *Slots - }{ - { - name: "no sub", - s: slotsA, - args: args{n: slotsB}, - want: &Slots{}, - }, - { - name: "sub", - s: slotsA, - args: args{n: slotsC}, - want: slotsDiff, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.s.Sub(tt.args.n); !reflect.DeepEqual(got.data, tt.want.data) { - t.Errorf("Slots.Sub() = %v, want %v", got.Slots(), tt.want.Slots()) - } - }) - } -} - -func TestSlots_Load(t *testing.T) { - slots := NewSlots() - // slots.Load([]string{"1-100", "1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1", "77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca"}) - type args struct { - v interface{} - } - tests := []struct { - name string - slots *Slots - args args - wantErr bool - }{ - { - name: "load", - args: args{ - v: []string{"5000-5100", "[1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1]", "[77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca]"}, - }, - wantErr: false, - }, - { - name: "load more", - args: args{ - v: []string{"5000-5100,10000-16666"}, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := slots.Load(tt.args.v); (err != nil) != tt.wantErr { - t.Errorf("Slots.Load() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestSlots_Count(t *testing.T) { - slots := NewSlots() - slots.Load([]string{"1-100", "[1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1]", "77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca]"}) - - type args struct { - status SlotAssignStatus - } - tests := []struct { - name string - slots *Slots - args args - wantC int - }{ - { - name: "assigned", - slots: slots, - args: args{ - status: SlotAssigned, - }, - wantC: 100, - }, - { - name: "importing", - slots: slots, - args: args{ - status: SlotImporting, - }, - wantC: 1, - }, - { - name: "migrating", - slots: slots, - args: args{ - status: SlotMigrating, - }, - wantC: 1, - }, - { - name: "unassigned", - slots: slots, - args: args{ - status: SlotUnAssigned, - }, - wantC: 16284, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotC := tt.slots.Count(tt.args.status); gotC != tt.wantC { - t.Errorf("Slots.Count() = %v, want %v. %v", gotC, tt.wantC, tt.slots.Slots()) - } - }) - } -} diff --git a/pkg/types/user/rule.go b/pkg/types/user/rule.go new file mode 100644 index 0000000..7b923e9 --- /dev/null +++ b/pkg/types/user/rule.go @@ -0,0 +1,302 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package user + +import ( + "fmt" + "slices" + "strings" +) + +var ( + allowedCategories = []string{ + "keyspace", "read", "write", "set", "sortedset", "list", "hash", "string", + "bitmap", "hyperloglog", "geo", "stream", "pubsub", "admin", "fast", "slow", + "blocking", "dangerous", "connection", "transaction", "scripting", "all", + } +) + +// Rule acl rules +// +// This rule supports redis 7.0, which is compatable with 6.0 +type Rule struct { + // Categories + Categories []string `json:"categories,omitempty"` + // DisallowedCategories + DisallowedCategories []string `json:"disallowedCategories,omitempty"` + // AllowedCommands supports and | + AllowedCommands []string `json:"allowedCommands,omitempty"` + // DisallowedCommands supports and | + DisallowedCommands []string `json:"disallowedCommands,omitempty"` + // KeyPatterns support multi patterns + KeyPatterns []string `json:"keyPatterns,omitempty"` + // KeyReadPatterns >= 7.0 support key read patterns + KeyReadPatterns []string `json:"keyReadPatterns,omitempty"` + // KeyWritePatterns >= 7.0 support key write patterns + KeyWritePatterns []string `json:"keyWritePatterns,omitempty"` + // Channels >= 7.0 support channel patterns + Channels []string `json:"channels,omitempty"` +} + +// NewRule +func NewRule(val string) (*Rule, error) { + if val == "" { + return &Rule{}, nil + } + + r := Rule{} + for _, v := range strings.Fields(val) { + if strings.ToLower(v) == "allcommands" { + if !slices.Contains(r.Categories, "all") { + r.Categories = append([]string{"all"}, r.Categories...) + } + } else if strings.ToLower(v) == "nocommands" { + if !slices.Contains(r.DisallowedCategories, "all") { + r.DisallowedCategories = append([]string{"all"}, r.DisallowedCategories...) + } + } else if strings.HasPrefix(v, "+@") { + v = strings.TrimPrefix(v, "+@") + if !slices.Contains(r.Categories, v) { + r.Categories = append(r.Categories, v) + } + } else if strings.HasPrefix(v, "-@") { + v = strings.TrimPrefix(v, "-@") + if !slices.Contains(r.DisallowedCategories, v) { + r.DisallowedCategories = append(r.DisallowedCategories, v) + } + } else if strings.HasPrefix(v, "-") { + v = strings.TrimPrefix(v, "-") + if !slices.Contains(r.DisallowedCommands, v) { + r.DisallowedCommands = append(r.DisallowedCommands, v) + } + } else if strings.HasPrefix(v, "+") { + v = strings.TrimPrefix(v, "+") + if !slices.Contains(r.AllowedCommands, v) { + r.AllowedCommands = append(r.AllowedCommands, v) + } + } else if strings.ToLower(v) == "allkeys" { + if !slices.Contains(r.KeyPatterns, "*") { + r.KeyPatterns = append([]string{"*"}, r.KeyPatterns...) + } + } else if strings.HasPrefix(v, "~") { + v = strings.TrimPrefix(v, "~") + if !slices.Contains(r.KeyPatterns, v) { + r.KeyPatterns = append(r.KeyPatterns, v) + } + } else if strings.HasPrefix(v, "%R~") { + v = strings.TrimPrefix(v, "%R~") + if !slices.Contains(r.KeyReadPatterns, v) { + r.KeyReadPatterns = append(r.KeyReadPatterns, v) + } + } else if strings.HasPrefix(v, "%W~") { + v = strings.TrimPrefix(v, "%W~") + if !slices.Contains(r.KeyWritePatterns, v) { + r.KeyWritePatterns = append(r.KeyWritePatterns, v) + } + } else if strings.ToLower(v) == "allchannels" { + if !slices.Contains(r.Channels, "*") { + r.Channels = append([]string{"*"}, r.Channels...) + } + } else if strings.HasPrefix(v, "&") { + v = strings.TrimPrefix(v, "&") + if !slices.Contains(r.Channels, v) { + r.Channels = append(r.Channels, v) + } + } else { + return nil, fmt.Errorf("unsupported rule %s", v) + } + } + for _, cate := range append(append([]string{}, r.Categories...), r.DisallowedCategories...) { + if !slices.Contains(allowedCategories, cate) { + return nil, fmt.Errorf("unsupported category %s", cate) + } + } + return &r, nil +} + +// Patch redis cluster client required rules +func PatchRedisClusterClientRequiredRules(rule *Rule) *Rule { + clusterRules := []string{ + "cluster|slots", + "cluster|nodes", + "cluster|info", + "cluster|keyslot", + "cluster|getkeysinslot", + "cluster|countkeysinslot", + } + // remove required rules + cmds := rule.DisallowedCommands + rule.DisallowedCommands = rule.DisallowedCommands[:0] + for _, cmd := range cmds { + if slices.Contains(clusterRules, cmd) { + continue + } else { + rule.DisallowedCommands = append(rule.DisallowedCommands, cmd) + } + } + requiredRules := map[string]bool{} + for _, cmd := range clusterRules { + requiredRules[cmd] = false + if rule.IsCommandEnabled(cmd, nil) { + requiredRules[cmd] = true + } + } + if rule.IsCommandEnabled("cluster", []string{"all", "admin", "slow", "dangerous"}) { + for key := range requiredRules { + requiredRules[key] = true + } + } + for _, cmd := range clusterRules { + if !requiredRules[cmd] { + rule.AllowedCommands = append(rule.AllowedCommands, cmd) + } + } + return rule +} + +func PatchRedisPubsubRules(rule *Rule) *Rule { + if len(rule.Channels) > 0 { + return rule + } + + cmds := map[string][]string{ + "psubscribe": {"all", "pubsub", "slow"}, + "publish": {"all", "pubsub", "fast"}, + "pubsub": {"all", "slow"}, + "pubsub|numpat": {"all", "pubsub", "slow"}, + "pubsub|channels": {"all", "pubsub", "slow"}, + "pubsub|numsub": {"all", "pubsub", "slow"}, + "pubsub|shardnumsub": {"all", "pubsub", "slow"}, + "pubsub|shardchannels": {"all", "pubsub", "slow"}, + "punsubscribe": {"all", "pubsub", "slow"}, + "spublish": {"all", "pubsub", "fast"}, + "ssubscribe": {"all", "pubsub", "slow"}, + "subscribe": {"all", "pubsub", "slow"}, + "sunsubscribe": {"all", "pubsub", "slow"}, + "unsubscribe": {"all", "pubsub", "slow"}, + } + isAnyEnabled := false + for cmd, cates := range cmds { + if rule.IsCommandEnabled(cmd, cates) { + isAnyEnabled = true + break + } + } + if isAnyEnabled { + rule.Channels = append(rule.Channels, "*") + } + return rule +} + +func (rule *Rule) Encode() string { + var ( + args []string + enabledAllCmd bool + disabledAllCmd bool + ) + for _, cate := range rule.Categories { + if cate == "all" { + enabledAllCmd = true + continue + } + args = append(args, fmt.Sprintf("+@%s", cate)) + } + for _, cate := range rule.DisallowedCategories { + if cate == "all" { + disabledAllCmd = true + continue + } + args = append(args, fmt.Sprintf("-@%s", cate)) + } + var subCmds []string + for _, cmd := range rule.AllowedCommands { + if strings.Contains(cmd, "|") { + subCmds = append(subCmds, fmt.Sprintf("+%s", cmd)) + } else { + args = append(args, fmt.Sprintf("+%s", cmd)) + } + } + for _, cmd := range rule.DisallowedCommands { + args = append(args, fmt.Sprintf("-%s", cmd)) + } + args = append(args, subCmds...) + + if disabledAllCmd { + args = append([]string{"-@all"}, args...) + } + if enabledAllCmd { + args = append([]string{"+@all"}, args...) + } + for _, pattern := range rule.KeyPatterns { + args = append(args, fmt.Sprintf("~%s", pattern)) + } + for _, pattern := range rule.KeyReadPatterns { + args = append(args, fmt.Sprintf("%%R~%s", pattern)) + } + for _, pattern := range rule.KeyWritePatterns { + args = append(args, fmt.Sprintf("%%W~%s", pattern)) + } + for _, pattern := range rule.Channels { + args = append(args, fmt.Sprintf("&%s", pattern)) + } + return strings.Join(args, " ") +} + +func (r *Rule) IsCommandEnabled(cmd string, cates []string) bool { + if slices.Contains(r.DisallowedCommands, cmd) { + return false + } + if slices.Contains(r.AllowedCommands, cmd) { + return true + } + for _, cate := range cates { + if slices.Contains(r.DisallowedCategories, cate) { + return false + } + } + for _, cate := range cates { + if slices.Contains(r.Categories, cate) { + return true + } + } + return false +} + +func (r *Rule) Validate(disableACL bool) error { + for _, cate := range append(append([]string{}, r.Categories...), r.DisallowedCategories...) { + if !slices.Contains(allowedCategories, cate) { + return fmt.Errorf("unsupported category %s", cate) + } + } + if len(r.Categories) == 0 && len(r.AllowedCommands) == 0 { + return fmt.Errorf("at least one category or command should be enabled") + } + if len(r.KeyPatterns) == 0 && len(r.KeyReadPatterns) == 0 && len(r.KeyWritePatterns) == 0 && len(r.Channels) == 0 { + return fmt.Errorf("at least one key pattern or channel pattern should be enabled") + } + if disableACL { + if r.IsCommandEnabled("acl", []string{"all", "admin", "slow", "dangerous"}) { + return fmt.Errorf("`acl` and it's sub commands are enabled") + } + for _, cmd := range r.AllowedCommands { + if strings.HasPrefix(cmd, "acl|") { + return fmt.Errorf("`acl` and it's sub commands are enabled") + } + } + } + return nil +} diff --git a/pkg/types/user/rule_test.go b/pkg/types/user/rule_test.go new file mode 100644 index 0000000..0db4e14 --- /dev/null +++ b/pkg/types/user/rule_test.go @@ -0,0 +1,443 @@ +/* +Copyright 2023 The RedisOperator Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package user + +import ( + "testing" +) + +func TestNewRule(t *testing.T) { + tests := []struct { + name string + raw string + want string + wantErr bool + }{ + { + name: "empty", + raw: "", + want: "", + wantErr: false, + }, + { + name: "allcommands", + raw: "allcommands", + want: "+@all", + wantErr: false, + }, + { + name: "nocommands", + raw: "nocommands", + want: "-@all", + wantErr: false, + }, + { + name: "allchannels", + raw: "allchannels", + want: "&*", + wantErr: false, + }, + { + name: "read key", + raw: "%R~test", + want: "%R~test", + wantErr: false, + }, + { + name: "write key", + raw: "%W~test", + want: "%W~test", + wantErr: false, + }, + { + name: "test", + raw: "allkeys +@all -flushall -flushdb", + want: "+@all -flushall -flushdb ~*", + wantErr: false, + }, + { + name: "notdangerous", + raw: "allkeys +@all -@dangerous", + want: "+@all -@dangerous ~*", + wantErr: false, + }, + { + name: "readwrite", + raw: "allkeys -@all +@write +@read -@dangerous", + want: "-@all +@write +@read -@dangerous ~*", + wantErr: false, + }, + { + name: "readonly", + raw: "allkeys -@all +@read -keys", + want: "-@all +@read -keys ~*", + wantErr: false, + }, + { + name: "administrator", + raw: "allkeys +@all -acl", + want: "+@all -acl ~*", + wantErr: false, + }, + { + name: "support subcommand", + raw: "allkeys -@admin +config|get", + want: "-@admin +config|get ~*", + wantErr: false, + }, + { + name: "disable cmd enable subcommand", + raw: "allkeys -config +config|get", + want: "-config +config|get ~*", + wantErr: false, + }, + { + name: "fixed acl", + raw: "+@all -acl +acl|setuser -flushall -flushdb -keys ~* &*", + want: "+@all -acl -flushall -flushdb -keys +acl|setuser ~* &*", + wantErr: false, + }, + { + name: "withpassword", + raw: "allkeys +@all >admin@123", + wantErr: true, + }, + { + name: "sanitize-payload", + raw: "allkeys +@all sanitize-payload", + wantErr: true, + }, + { + name: "skip-sanitize-payload", + raw: "allkeys +@all skip-sanitize-payload", + wantErr: true, + }, + { + name: "not allowed category", + raw: "+@test", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule, err := NewRule(tt.raw) + if (err != nil) != tt.wantErr { + t.Errorf("NewRule() error = %v, wantErr %v", err, tt.wantErr) + return + } + if rule != nil { + if rule.Encode() != tt.want { + t.Errorf("NewRule() = %v, want %v", rule, tt.want) + } + } + }) + } +} + +func TestRule_Format(t *testing.T) { + tests := []struct { + name string + raw string + want string + wantErr bool + }{ + { + name: "test", + raw: "allkeys +@all -flushall -flushdb", + want: "+@all -flushall -flushdb ~*", + wantErr: false, + }, + { + name: "notdangerous", + raw: "allkeys +@all -@dangerous", + want: "+@all -@dangerous ~*", + wantErr: false, + }, + { + name: "readwrite", + raw: "allkeys -@all +@write +@read -@dangerous", + want: "-@all +@write +@read -@dangerous ~*", + wantErr: false, + }, + { + name: "readonly", + raw: "allkeys -@all +@read -keys", + want: "-@all +@read -keys ~*", + wantErr: false, + }, + { + name: "administrator", + raw: "allkeys +@all -acl", + want: "+@all -acl ~*", + wantErr: false, + }, + { + name: "support subcommand", + raw: "allkeys -@admin +config|get", + want: "-@admin +config|get ~*", + wantErr: false, + }, + { + name: "disable cmd enable subcommand", + raw: "allkeys -config +config|get", + want: "-config +config|get ~*", + wantErr: false, + }, + { + name: "fixed acl", + raw: "+@all -acl +acl|setuser -flushall -flushdb -keys ~* &*", + want: "+@all -acl -flushall -flushdb -keys +acl|setuser ~* &*", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule, err := NewRule(tt.raw) + if (err != nil) != tt.wantErr { + t.Errorf("NewRule() error = %v, wantErr: %v", err, tt.wantErr) + return + } + if rule != nil { + if got := rule.Encode(); got != tt.want { + t.Errorf("Rule.Encode() = %v, want %v", got, tt.want) + } + } + }) + } +} + +func TestRule_Validate(t *testing.T) { + tests := []struct { + name string + raw string + disableAcl bool + wantErr bool + }{ + { + name: "test", + raw: "allkeys +@all -flushall -flushdb", + disableAcl: true, + wantErr: true, + }, + { + name: "notdangerous", + raw: "allkeys +@all -@dangerous", + disableAcl: true, + wantErr: false, + }, + { + name: "readwrite", + raw: "allkeys -@all +@write +@read -@dangerous", + disableAcl: true, + wantErr: false, + }, + { + name: "readonly", + raw: "allkeys -@all +@read -keys", + disableAcl: true, + wantErr: false, + }, + { + name: "administrator", + raw: "allkeys +@all -acl", + disableAcl: true, + wantErr: false, + }, + { + name: "support subcommand", + raw: "allkeys -@admin +config|get", + disableAcl: true, + wantErr: false, + }, + { + name: "disable cmd enable subcommand", + raw: "allkeys -config +config|get", + disableAcl: true, + wantErr: false, + }, + { + name: "default user for 7.0", + raw: "+@all -acl -flushall -flushdb -keys ~* &*", + disableAcl: true, + wantErr: false, + }, + { + name: "fixed acl", + raw: "+@all -acl +acl|setuser -flushall -flushdb -keys ~* &*", + disableAcl: true, + wantErr: true, + }, + { + name: "fixed acl", + raw: "+@all -acl +acl|setuser -flushall -flushdb -keys ~* &*", + disableAcl: true, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := NewRule(tt.raw) + if err != nil { + t.Errorf("NewRule() error = %v, ", err) + return + } + if err := r.Validate(tt.disableAcl); (err != nil) != tt.wantErr { + t.Errorf("Rule.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPatchRedisClusterClientRequiredRules(t *testing.T) { + tests := []struct { + name string + raw string + want string + wantErr bool + }{ + { + name: "test", + raw: "allkeys +@all -flushall -flushdb", + want: "+@all -flushall -flushdb ~*", + wantErr: false, + }, + { + name: "notdangerous", + raw: "allkeys +@all -@dangerous", + want: "+@all -@dangerous +cluster|slots +cluster|nodes +cluster|info +cluster|keyslot +cluster|getkeysinslot +cluster|countkeysinslot ~*", + wantErr: false, + }, + { + name: "readwrite", + raw: "allkeys -@all +@write +@read -@dangerous", + want: "-@all +@write +@read -@dangerous +cluster|slots +cluster|nodes +cluster|info +cluster|keyslot +cluster|getkeysinslot +cluster|countkeysinslot ~*", + wantErr: false, + }, + { + name: "readonly", + raw: "allkeys -@all +@read -keys", + want: "-@all +@read -keys +cluster|slots +cluster|nodes +cluster|info +cluster|keyslot +cluster|getkeysinslot +cluster|countkeysinslot ~*", + wantErr: false, + }, + { + name: "administrator", + raw: "allkeys +@all -acl", + want: "+@all -acl ~*", + wantErr: false, + }, + { + name: "support subcommand", + raw: "allkeys -@admin +config|get", + want: "-@admin +config|get +cluster|slots +cluster|nodes +cluster|info +cluster|keyslot +cluster|getkeysinslot +cluster|countkeysinslot ~*", + wantErr: false, + }, + { + name: "disable cmd enable subcommand", + raw: "allkeys -config +config|get", + want: "-config +config|get +cluster|slots +cluster|nodes +cluster|info +cluster|keyslot +cluster|getkeysinslot +cluster|countkeysinslot ~*", + wantErr: false, + }, + { + name: "fixed acl", + raw: "+@all -acl +acl|setuser -flushall -flushdb -keys ~* &*", + want: "+@all -acl -flushall -flushdb -keys +acl|setuser ~* &*", + wantErr: false, + }, + { + name: "disable cluster", + raw: "+@all -cluster ~* &*", + want: "+@all -cluster +cluster|slots +cluster|nodes +cluster|info +cluster|keyslot +cluster|getkeysinslot +cluster|countkeysinslot ~* &*", + wantErr: false, + }, + { + name: "disable cluster subcommand", + raw: "+@all -cluster|nodes ~* &*", + want: "+@all ~* &*", + wantErr: false, + }, + { + name: "enable cluster subcommand", + raw: "+@all +cluster -cluster|nodes ~* &*", + want: "+@all +cluster ~* &*", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule, err := NewRule(tt.raw) + if (err != nil) != tt.wantErr { + t.Errorf("NewRule() error = %v, wantErr %v", err, tt.wantErr) + return + } + if rule != nil { + rule = PatchRedisClusterClientRequiredRules(rule) + if got := rule.Encode(); got != tt.want { + t.Errorf("PatchRedisClusterClientRequiredRules() = %v, want %v", got, tt.want) + } + } + }) + } +} + +func TestPatchRedisPubsubRules(t *testing.T) { + tests := []struct { + name string + raw string + want string + wantErr bool + }{ + { + name: "empty", + raw: "", + want: "", + wantErr: false, + }, + { + name: "channel enabled indirect", + raw: "allkeys +@all -flushall -flushdb", + want: "+@all -flushall -flushdb ~* &*", + wantErr: false, + }, + { + name: "channel not enabled", + raw: "allkeys +get +set -flushall -flushdb", + want: "+get +set -flushall -flushdb ~*", + wantErr: false, + }, + { + name: "channel enabled", + raw: "allkeys +get +set -flushall -flushdb &*", + want: "+get +set -flushall -flushdb ~* &*", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule, err := NewRule(tt.raw) + if (err != nil) != tt.wantErr { + t.Errorf("NewRule() error = %v, wantErr %v", err, tt.wantErr) + return + } + if rule != nil { + rule = PatchRedisPubsubRules(rule) + if got := rule.Encode(); got != tt.want { + t.Errorf("PatchRedisPubsubRules() = %v, want %v", got, tt.want) + } + } + }) + } +} diff --git a/pkg/types/user/user.go b/pkg/types/user/user.go index 455a869..3b1b1d2 100644 --- a/pkg/types/user/user.go +++ b/pkg/types/user/user.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,7 +17,6 @@ limitations under the License. package user import ( - "errors" "fmt" "regexp" "strings" @@ -35,74 +34,6 @@ const ( PasswordSecretKey = "password" ) -// Rule acl rules -// -// This rule supports redis 7.0, which is compatable with 6.0 -type Rule struct { - // Categories - Categories []string `json:"categories,omitempty"` - DisallowedCategories []string `json:"disallowedCategories,omitempty"` - // AllowedCommands supports and | - AllowedCommands []string `json:"allowedCommands,omitempty"` - // DisallowedCommands supports and | - DisallowedCommands []string `json:"disallowedCommands,omitempty"` - // KeyPatterns support multi patterns, for 7.0 support %R~ and %W~ patterns - KeyPatterns []string `json:"keyPatterns,omitempty"` - Channels []string `json:"channels,omitempty"` -} - -func (r *Rule) Validate() error { - if r == nil { - return errors.New("nil rule") - } - if len(r.Categories) == 0 && len(r.AllowedCommands) == 0 { - return errors.New("invalid rule, no allowed command") - } - if len(r.KeyPatterns) == 0 { - return errors.New("invalid rule, no key pattern") - } - return nil -} - -func (r *Rule) String() string { - return strings.Join(append(append(append(append([]string{}, r.Categories...), - r.AllowedCommands...), r.DisallowedCommands...), r.KeyPatterns...), " ") -} - -func (r *Rule) Parse(ruleString string) error { - if r == nil { - r = &Rule{} - } - if ruleString == "" { - return nil - } - for _, v := range strings.Split(ruleString, " ") { - if v == "" { - continue - } - if strings.HasPrefix(v, "+@") { - r.Categories = append(r.Categories, strings.TrimPrefix(v, "+@")) - } else if strings.HasPrefix(v, "-@") { - r.DisallowedCategories = append(r.DisallowedCategories, strings.TrimPrefix(v, "-@")) - } else if strings.HasPrefix(v, "-") { - r.DisallowedCommands = append(r.DisallowedCommands, strings.TrimPrefix(v, "-")) - } else if strings.HasPrefix(v, "+") { - r.AllowedCommands = append(r.AllowedCommands, strings.TrimPrefix(v, "+")) - } else if strings.HasPrefix(v, "~") { - r.KeyPatterns = append(r.KeyPatterns, strings.TrimPrefix(v, "~")) - } else if strings.HasPrefix(v, "&") { - r.Channels = append(r.Channels, strings.TrimPrefix(v, "&")) - } else if v == "allkeys" { - r.KeyPatterns = append(r.KeyPatterns, "*") - } else if v == "resetkeys" { - r.KeyPatterns = append(r.KeyPatterns, "*") - } else { - return fmt.Errorf("invalid rule string %s", v) - } - } - return nil -} - // UserRole type UserRole string @@ -112,9 +43,9 @@ const ( ) // NewOperatorUser -func NewOperatorUser(secret *v1.Secret, ACL2Support bool) (*User, error) { +func NewOperatorUser(secret *v1.Secret, acl2Support bool) (*User, error) { rule := Rule{Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}} - if ACL2Support { + if acl2Support { rule.Channels = []string{"*"} } user := User{ @@ -135,7 +66,7 @@ func NewOperatorUser(secret *v1.Secret, ACL2Support bool) (*User, error) { } // NewUser -func NewUser(name string, role UserRole, secret *v1.Secret) (*User, error) { +func NewUser(name string, role UserRole, secret *v1.Secret, acl2Support bool) (*User, error) { var ( err error passwd *Password @@ -146,13 +77,20 @@ func NewUser(name string, role UserRole, secret *v1.Secret) (*User, error) { } } Rules := []*Rule{{Categories: []string{"all"}, KeyPatterns: []string{"*"}}} - if name == "" { + if name == "" || name == DefaultUserName { name = DefaultUserName - Rules = []*Rule{{Categories: []string{"all"}, - KeyPatterns: []string{"*"}, - DisallowedCategories: []string{"dangerous"}, - DisallowedCommands: []string{"acl"}}} + Rules = []*Rule{ + { + Categories: []string{"all"}, + KeyPatterns: []string{"*"}, + DisallowedCommands: []string{"acl", "flushall", "flushdb", "keys"}, + }, + } + } + if acl2Support { + Rules[0].Channels = []string{"*"} } + user := &User{ Name: name, Role: role, @@ -165,21 +103,48 @@ func NewUser(name string, role UserRole, secret *v1.Secret) (*User, error) { return user, nil } -func NewUserFromRedisUser(username, ruleStr string, password_obj *Password) (*User, error) { - rule := Rule{} +// NewSentinelUser +func NewSentinelUser(name string, role UserRole, secret *v1.Secret) (*User, error) { + var ( + err error + passwd *Password + ) + if secret != nil { + if passwd, err = NewPassword(secret); err != nil { + return nil, err + } + } + + user := &User{ + Name: name, + Role: role, + Password: passwd, + } + if err := user.Validate(); err != nil { + return nil, err + } + return user, nil +} + +func NewUserFromRedisUser(username, ruleStr string, pwd *Password) (*User, error) { rules := []*Rule{} if ruleStr != "" { - err := rule.Parse(ruleStr) + rule, err := NewRule(ruleStr) if err != nil { return nil, err } - rules = append(rules, &rule) + rules = append(rules, rule) + } + role := RoleDeveloper + if username == DefaultOperatorUserName { + role = RoleOperator } - user := User{Name: username, - Role: RoleDeveloper, + user := User{ + Name: username, + Role: role, Rules: rules, - Password: password_obj, + Password: pwd, } return &user, nil @@ -209,11 +174,6 @@ func (u *User) AppendRule(rules ...*Rule) error { if u == nil { return nil } - for _, rule := range rules { - if err := rule.Validate(); err != nil { - return err - } - } u.Rules = append(u.Rules, rules...) return nil } @@ -240,7 +200,7 @@ func (u *User) String() string { vals := []string{u.Name, string(u.Role)} for _, rule := range u.Rules { - vals = append(vals, rule.String()) + vals = append(vals, rule.Encode()) } return strings.Join(vals, " ") } diff --git a/pkg/types/user/user_test.go b/pkg/types/user/user_test.go index d6fdb8f..33a9506 100644 --- a/pkg/types/user/user_test.go +++ b/pkg/types/user/user_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -19,37 +19,833 @@ package user import ( "reflect" "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + validSecret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Data: map[string][]byte{ + "password": []byte("password"), + }, + } + invalidSecret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Data: map[string][]byte{}, + } ) -// Add the Rule struct and Parse function here... +func TestNewOperatorUser(t *testing.T) { + type args struct { + secret *v1.Secret + acl2Support bool + } + tests := []struct { + name string + args args + want *User + wantErr bool + }{ + { + name: "without secret, disable acl2", + args: args{ + secret: nil, + acl2Support: false, + }, + want: &User{ + Name: DefaultOperatorUserName, + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "without secret, enable acl2", + args: args{ + secret: nil, + acl2Support: true, + }, + want: &User{ + Name: DefaultOperatorUserName, + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}, Channels: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "with secret, enable acl2", + args: args{ + secret: validSecret, + acl2Support: true, + }, + want: &User{ + Name: DefaultOperatorUserName, + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}, Channels: []string{"*"}}, + }, + Password: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + }, + wantErr: false, + }, + { + name: "with invalid secret, enable acl2", + args: args{ + secret: invalidSecret, + acl2Support: true, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewOperatorUser(tt.args.secret, tt.args.acl2Support) + if (err != nil) != tt.wantErr { + t.Errorf("NewOperatorUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewOperatorUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewUser(t *testing.T) { + type args struct { + name string + role UserRole + secret *v1.Secret + acl2Support bool + } + tests := []struct { + name string + args args + want *User + wantErr bool + }{ + { + name: "default user, disable acl2", + args: args{ + name: "", + role: RoleDeveloper, + secret: nil, + acl2Support: false, + }, + want: &User{ + Name: DefaultUserName, + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"acl", "flushall", "flushdb", "keys"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "default user with name and disable acl2", + args: args{ + name: DefaultUserName, + role: RoleDeveloper, + secret: nil, + acl2Support: false, + }, + want: &User{ + Name: DefaultUserName, + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"acl", "flushall", "flushdb", "keys"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "default user, enable acl2", + args: args{ + name: "", + role: RoleDeveloper, + secret: nil, + acl2Support: true, + }, + want: &User{ + Name: DefaultUserName, + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"acl", "flushall", "flushdb", "keys"}, + KeyPatterns: []string{"*"}, Channels: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "custom user, disable acl2", + args: args{ + name: "debug", + role: RoleDeveloper, + secret: nil, + acl2Support: false, + }, + want: &User{ + Name: "debug", + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "custom user, enable acl2", + args: args{ + name: "debug", + role: RoleDeveloper, + secret: nil, + acl2Support: true, + }, + want: &User{ + Name: "debug", + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, KeyPatterns: []string{"*"}, Channels: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "operator user, enable acl2", + args: args{ + name: DefaultOperatorUserName, + role: RoleOperator, + secret: nil, + acl2Support: true, + }, + want: &User{ + Name: DefaultOperatorUserName, + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, KeyPatterns: []string{"*"}, Channels: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "default user with secret", + args: args{ + name: DefaultUserName, + role: RoleDeveloper, + secret: validSecret, + acl2Support: true, + }, + want: &User{ + Name: DefaultUserName, + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"acl", "flushall", "flushdb", "keys"}, + KeyPatterns: []string{"*"}, Channels: []string{"*"}}, + }, + Password: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + }, + wantErr: false, + }, + { + name: "with invalid secret, enable acl2", + args: args{ + name: DefaultUserName, + role: RoleDeveloper, + secret: invalidSecret, + acl2Support: true, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewUser(tt.args.name, tt.args.role, tt.args.secret, tt.args.acl2Support) + if (err != nil) != tt.wantErr { + t.Errorf("NewUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewSentinelUser(t *testing.T) { + type args struct { + name string + role UserRole + secret *v1.Secret + } + tests := []struct { + name string + args args + want *User + wantErr bool + }{ + { + name: "custom user without secret", + args: args{ + name: "", + role: RoleDeveloper, + secret: nil, + }, + want: &User{ + Name: "", + Role: RoleDeveloper, + }, + wantErr: false, + }, + { + name: "custom user with secret", + args: args{ + name: "test", + role: RoleDeveloper, + secret: validSecret, + }, + want: &User{ + Name: "test", + Role: RoleDeveloper, + Password: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + }, + wantErr: false, + }, + { + name: "with invalid secret, enable acl2", + args: args{ + name: "test1", + role: RoleDeveloper, + secret: invalidSecret, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewSentinelUser(tt.args.name, tt.args.role, tt.args.secret) + if (err != nil) != tt.wantErr { + t.Errorf("NewSentinelUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewSentinelUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewUserFromRedisUser(t *testing.T) { + type args struct { + username string + ruleStr string + pwd *Password + } + tests := []struct { + name string + args args + want *User + wantErr bool + }{ + { + name: "default user", + args: args{ + username: DefaultUserName, + ruleStr: "+@all -flushall -flushdb -keys ~*", + pwd: nil, + }, + want: &User{ + Name: DefaultUserName, + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"flushall", "flushdb", "keys"}, KeyPatterns: []string{"*"}}, + }, + }, + }, + { + name: "invald user", + args: args{ + username: DefaultUserName, + ruleStr: "+@all -flushall -flushdb -keys ~* +@test", + pwd: nil, + }, + wantErr: true, + }, + { + name: "operator user", + args: args{ + username: DefaultOperatorUserName, + ruleStr: "+@all -keys ~*", + pwd: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + }, + want: &User{ + Name: DefaultOperatorUserName, + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}}, + }, + Password: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewUserFromRedisUser(tt.args.username, tt.args.ruleStr, tt.args.pwd) + if (err != nil) != tt.wantErr { + t.Errorf("NewUserFromRedisUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewUserFromRedisUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUser_GetPassword(t *testing.T) { + type fields struct { + Name string + Role UserRole + Password *Password + Rules []*Rule + } + tests := []struct { + name string + fields fields + want *Password + }{ + { + name: "without password", + fields: fields{}, + want: nil, + }, + { + name: "with password", + fields: fields{ + Password: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + }, + want: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := &User{ + Name: tt.fields.Name, + Role: tt.fields.Role, + Password: tt.fields.Password, + Rules: tt.fields.Rules, + } + if got := u.GetPassword(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("User.GetPassword() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUser_AppendRule(t *testing.T) { + type fields struct { + Name string + Role UserRole + Password *Password + Rules []*Rule + } + type args struct { + rules []*Rule + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "nil append", + fields: fields{ + Rules: []*Rule{}, + }, + args: args{rules: []*Rule{}}, + wantErr: false, + }, + { + name: "append", + fields: fields{ + Rules: []*Rule{}, + }, + args: args{rules: []*Rule{{Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}}}}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := &User{ + Name: tt.fields.Name, + Role: tt.fields.Role, + Password: tt.fields.Password, + Rules: tt.fields.Rules, + } + if err := u.AppendRule(tt.args.rules...); (err != nil) != tt.wantErr { + t.Errorf("User.AppendRule() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} -func TestParse(t *testing.T) { - // Create Rule struct instance - rule := Rule{} +func TestUser_Validate(t *testing.T) { + type fields struct { + Name string + Role UserRole + Password *Password + Rules []*Rule + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "default user", + fields: fields{ + Name: DefaultUserName, + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"acl", "flushall", "flushdb", "keys"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "operator user", + fields: fields{ + Name: DefaultOperatorUserName, + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: false, + }, + { + name: "invalid username", + fields: fields{ + Name: "adwf_13123r", + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: true, + }, + { + name: "invalid user role", + fields: fields{ + Name: "adwf", + Role: "test", + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := &User{ + Name: tt.fields.Name, + Role: tt.fields.Role, + Password: tt.fields.Password, + Rules: tt.fields.Rules, + } + if err := u.Validate(); (err != nil) != tt.wantErr { + t.Errorf("User.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} - // Test case - testCase := "allkeys ~test &* +@all +get -set" +func TestUser_String(t *testing.T) { + type fields struct { + Name string + Role UserRole + Password *Password + Rules []*Rule + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "default user", + fields: fields{ + Name: DefaultUserName, + Role: RoleDeveloper, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"acl", "flushall", "flushdb", "keys"}, KeyPatterns: []string{"*"}}, + }, + }, + want: "default Developer +@all -acl -flushall -flushdb -keys ~*", + }, + { + name: "operator user", + fields: fields{ + Name: DefaultOperatorUserName, + Role: RoleOperator, + Rules: []*Rule{ + {Categories: []string{"all"}, DisallowedCommands: []string{"keys"}, KeyPatterns: []string{"*"}}, + }, + }, + want: "operator Operator +@all -keys ~*", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := &User{ + Name: tt.fields.Name, + Role: tt.fields.Role, + Password: tt.fields.Password, + Rules: tt.fields.Rules, + } + if got := u.String(); got != tt.want { + t.Errorf("User.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewPassword(t *testing.T) { + type args struct { + secret *v1.Secret + } + tests := []struct { + name string + args args + want *Password + wantErr bool + }{ + { + name: "valid secret", + args: args{ + secret: validSecret, + }, + want: &Password{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + wantErr: false, + }, + { + name: "invalid secret", + args: args{ + secret: invalidSecret, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewPassword(tt.args.secret) + if (err != nil) != tt.wantErr { + t.Errorf("NewPassword() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewPassword() = %v, want %v", got, tt.want) + } + }) + } +} - // Call Parse function to parse the rule - err := rule.Parse(testCase) +func TestPassword_GetSecretName(t *testing.T) { + type fields struct { + SecretName string + secret *v1.Secret + data string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "without secret", + fields: fields{}, + want: "", + }, + { + name: "with secret", + fields: fields{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Password{ + SecretName: tt.fields.SecretName, + secret: tt.fields.secret, + data: tt.fields.data, + } + if got := p.GetSecretName(); got != tt.want { + t.Errorf("Password.GetSecretName() = %v, want %v", got, tt.want) + } + }) + } +} - // Check for errors - if err != nil { - t.Errorf("Error parsing rule: %v", err) +func TestPassword_SetSecret(t *testing.T) { + type args struct { + secret *v1.Secret + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid secret", + args: args{ + secret: validSecret, + }, + wantErr: false, + }, + { + name: "invalid secret", + args: args{ + secret: invalidSecret, + }, + wantErr: true, + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Password{} + if err := p.SetSecret(tt.args.secret); (err != nil) != tt.wantErr { + t.Errorf("Password.SetSecret() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} - // Check the expected values after parsing - expectedRule := Rule{ - Categories: []string{"all"}, - AllowedCommands: []string{"get"}, - DisallowedCommands: []string{"set"}, - DisallowedCategories: []string{""}, - KeyPatterns: []string{"allkeys", "test"}, - Channels: []string{"*"}, +func TestPassword_Secret(t *testing.T) { + type fields struct { + SecretName string + secret *v1.Secret + data string + } + tests := []struct { + name string + fields fields + want *v1.Secret + }{ + { + name: "without secret", + fields: fields{}, + want: nil, + }, + { + name: "with secret", + fields: fields{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + want: validSecret, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Password{ + SecretName: tt.fields.SecretName, + secret: tt.fields.secret, + data: tt.fields.data, + } + if got := p.Secret(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Password.Secret() = %v, want %v", got, tt.want) + } + }) } +} - // Compare the parsed rule with the expected rule - if reflect.DeepEqual(rule, expectedRule) { - t.Errorf("Parsed rule does not match the expected rule. Parsed: %+v, Expected: %+v", rule, expectedRule) +func TestPassword_String(t *testing.T) { + type fields struct { + SecretName string + secret *v1.Secret + data string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "without secret", + fields: fields{}, + want: "", + }, + { + name: "with secret", + fields: fields{ + SecretName: "test", + secret: validSecret, + data: "password", + }, + want: "password", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Password{ + SecretName: tt.fields.SecretName, + secret: tt.fields.secret, + data: tt.fields.data, + } + if got := p.String(); got != tt.want { + t.Errorf("Password.String() = %v, want %v", got, tt.want) + } + }) } } diff --git a/pkg/util/failover.go b/pkg/util/failover.go deleted file mode 100644 index 35da4cf..0000000 --- a/pkg/util/failover.go +++ /dev/null @@ -1,154 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "fmt" - "strings" - - v1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" -) - -const ( - BaseName = "rf" - SentinelName = "s" - SentinelRoleName = "sentinel" - SentinelConfigFileName = "sentinel.conf" - RedisConfigFileName = "redis.conf" - RedisName = "r" - RedisShutdownName = "r-s" - RedisReadinessName = "r-readiness" - RedisRoleName = "redis" - RedisMasterName = "mymaster" - AppLabel = "redis-failover" - HostnameTopologyKey = "kubernetes.io/hostname" - RedisBackupServiceAccountName = "redis-backup" - RedisBackupRoleName = "redis-backup" - RedisBackupRoleBindingName = "redis-backup" -) - -const ( - RedisConfigFileNameBackup = "redis.conf.bk" - RedisInitScript = "init.sh" - SentinelEntrypoint = "entrypoint.sh" - RedisBackupVolumeName = "backup-data" - S3SecretVolumeName = "s3-secret" -) - -// variables refering to the redis exporter port -const ( - ExporterPort = 9121 - SentinelExporterPort = 9355 - SentinelPort = "26379" - ExporterPortName = "http-metrics" - RedisPort = 6379 - RedisPortString = "6379" - RedisPortName = "redis" - ExporterContainerName = "redis-exporter" - SentinelExporterContainerName = "sentinel-exporter" - RestoreContainerName = "restore" - ExporterDefaultRequestCPU = "25m" - ExporterDefaultLimitCPU = "50m" - ExporterDefaultRequestMemory = "50Mi" - ExporterDefaultLimitMemory = "100Mi" -) - -// label -const ( - LabelInstanceName = "app.kubernetes.io/name" - LabelPartOf = "app.kubernetes.io/part-of" - LabelRedisConfig = "redis.middleware.alauda.io/config" - LabelRedisConfigValue = "true" - LabelRedisRole = "redis.middleware.alauda.io/role" -) - -// Redis arch -const ( - LabelRedisArch = "redisarch" -) - -// Redis role -const ( - Master = "master" - Slave = "slave" -) - -func GenerateName(typeName, metaName string) string { - return fmt.Sprintf("%s%s-%s", BaseName, typeName, metaName) -} - -func GetRedisName(rf *v1.RedisFailover) string { - return GenerateName(RedisName, rf.Name) -} - -func GetSentinelName(rf *v1.RedisFailover) string { - return GenerateName(SentinelName, rf.Name) -} - -func GetRedisShutdownName(rf *v1.RedisFailover) string { - return GenerateName(RedisShutdownName, rf.Name) -} - -func GetRedisNameExporter(rf *v1.RedisFailover) string { - return GenerateName(fmt.Sprintf("%s%s", RedisName, "e"), rf.Name) -} - -func GetSentinelHeadlessSvc(rf *v1.RedisFailover) string { - return GenerateName(SentinelName, fmt.Sprintf("%s-%s", rf.Name, "hl")) -} - -func GetRedisNodePortSvc(rf *v1.RedisFailover) string { - return GenerateName(fmt.Sprintf("%s-%s", RedisName, "n"), rf.Name) -} - -func GetRedisSecretName(rf *v1.RedisFailover) string { - return GenerateName(fmt.Sprintf("%s-%s", RedisName, "p"), rf.Name) -} - -func GetSentinelReadinessConfigmap(rf *v1.RedisFailover) string { - return GenerateName(fmt.Sprintf("%s-%s", SentinelName, "r"), rf.Name) -} - -func GetRedisShutdownConfigMapName(rf *v1.RedisFailover) string { - return GenerateName(fmt.Sprintf("%s-%s", RedisName, "s"), rf.Name) -} - -// GetRedisSSLSecretName return the name of redis ssl secret -func GetRedisSSLSecretName(rfName string) string { - return rfName + "-tls" -} - -func GetRedisRWServiceName(rfName string) string { - return GenerateName(RedisName, rfName) + "-read-write" -} - -func GetRedisROServiceName(rfName string) string { - return GenerateName(RedisName, rfName) + "-read-only" -} - -// split storage name, example: pvc/redisfailover-persistent-keep-data-rfr-redis-sentinel-demo-0 -func GetClaimName(backupDestination string) string { - names := strings.Split(backupDestination, "/") - if len(names) != 2 { - return "" - } - return names[1] -} - -func GetCronJobName(redisName, scheduleName string) string { - return fmt.Sprintf("%s-%s", redisName, scheduleName) -} diff --git a/pkg/util/map.go b/pkg/util/map.go deleted file mode 100644 index a02d725..0000000 --- a/pkg/util/map.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -// MergeLabels merges all the label maps received as argument into a single new label map. -func MergeMap(maps ...map[string]string) map[string]string { - ret := map[string]string{} - - for _, item := range maps { - for k, v := range item { - ret[k] = v - } - } - return ret -} - -func MapKeys(m map[string]string) (ret []string) { - for k := range m { - ret = append(ret, k) - } - return -} diff --git a/pkg/util/redis.go b/pkg/util/redis.go deleted file mode 100644 index f6620c7..0000000 --- a/pkg/util/redis.go +++ /dev/null @@ -1,184 +0,0 @@ -/* -Copyright 2023 The RedisOperator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "context" - "fmt" - "io" - "net" - "sync" - "time" - - databasesv1 "github.com/alauda/redis-operator/api/databases.spotahome.com/v1" - "github.com/alauda/redis-operator/api/redis.kun/v1alpha1" - "github.com/alauda/redis-operator/pkg/redis" - types "github.com/alauda/redis-operator/pkg/types/redis" - "github.com/go-logr/logr" - v1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func RedisInstancePersistence(ctx context.Context, mgrCli client.Client, namespace string, logger logr.Logger) error { - var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - - // cluster instance - var ( - listResp v1alpha1.DistributedRedisClusterList - cursor string - maxDbSize int64 - ) - for { - if err := mgrCli.List(ctx, &listResp, &client.ListOptions{Limit: 100, Namespace: namespace, Continue: cursor}); err != nil { - logger.Error(err, "load redis cluster instance failed") - break - } - - for _, ins := range listResp.Items { - if ins.Spec.Config["save"] == "" || ins.Spec.Storage == nil || ins.Spec.Storage.Size.IsZero() { - continue - } - logger.Info("do persistence for DistributedRedisCluster", "instance", ins.Name) - - var password string - if ins.Spec.PasswordSecret != nil && ins.Spec.PasswordSecret.Name != "" { - var secret v1.Secret - if err := mgrCli.Get(ctx, client.ObjectKey{ - Namespace: ins.Namespace, - Name: ins.Spec.PasswordSecret.Name, - }, &secret); err != nil { - logger.Error(err, "get redis password secret failed") - continue - } - password = string(secret.Data["password"]) - } - - for _, node := range ins.Status.Nodes { - if node.Role != types.RedisRoleMaster { - continue - } - func() { - redisCli := redis.NewRedisClient(net.JoinHostPort(node.IP, node.Port), redis.AuthConfig{ - Password: password, - }) - defer redisCli.Close() - - nctx, cancel := context.WithTimeout(ctx, time.Second*10) - defer cancel() - - info, _ := redisCli.Info(ctx) - if info != nil && info.UsedMemoryDataset > maxDbSize { - maxDbSize = info.UsedMemoryDataset - } - if _, err := redisCli.Do(nctx, "BGSAVE"); err != nil && err != redis.ErrNil && err != io.EOF { - logger.Error(err, "redis instance bgsave failed", "instance", ins.Name, "node", net.JoinHostPort(node.IP, node.Port)) - } - }() - } - } - - cursor = listResp.Continue - if cursor != "" { - continue - } - break - } - - duration := time.Duration((float64(maxDbSize)/1024/1024/1024)*10) * time.Second - if duration > 0 { - // do best effort to make sure persistence is done - logger.Info(fmt.Sprintf("wait %d for cluster persistence done", duration)) - time.Sleep(duration) - } - }() - - go func() { - defer wg.Done() - - // sentinel instance - var ( - listResp databasesv1.RedisFailoverList - cursor string - maxDbSize int64 - ) - for { - if err := mgrCli.List(ctx, &listResp, &client.ListOptions{Limit: 100, Namespace: namespace, Continue: cursor}); err != nil { - logger.Error(err, "load redis sentinel instance failed") - break - } - - for _, ins := range listResp.Items { - if ins.Spec.Redis.CustomConfig["save"] == "" || ins.Spec.Redis.Storage.PersistentVolumeClaim == nil { - continue - } - logger.Info("do persistence for RedisFailover", "instance", ins.Name) - - var password string - if ins.Spec.Auth.SecretPath != "" { - var secret v1.Secret - if err := mgrCli.Get(ctx, client.ObjectKey{ - Namespace: ins.Namespace, - Name: ins.Spec.Auth.SecretPath, - }, &secret); err != nil { - logger.Error(err, "get redis password secret failed") - continue - } - password = string(secret.Data["password"]) - } - if addr := ins.Status.Master.Address; addr != "" { - redisCli := redis.NewRedisClient(addr, redis.AuthConfig{ - Password: password, - }) - defer redisCli.Close() - - nctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - - info, _ := redisCli.Info(ctx) - if info != nil && info.UsedMemoryDataset > maxDbSize { - maxDbSize = info.UsedMemoryDataset - } - - if _, err := redisCli.Do(nctx, "BGSAVE"); err != nil && err != redis.ErrNil && err != io.EOF { - logger.Error(err, "redis instance bgsave failed", "instance", ins.Name, "node", addr) - } - } - } - - cursor = listResp.Continue - if cursor != "" { - continue - } - break - } - - duration := time.Duration((float64(maxDbSize)/1024/1024/1024)*10) * time.Second - if duration > 0 { - // do best effort to make sure persistence is done - logger.Info(fmt.Sprintf("wait %d for sentinel persistence done", duration)) - time.Sleep(duration) - } - }() - wg.Wait() - - time.Sleep(3 * time.Second) - - return nil -} diff --git a/tools/init.sh b/tools/init.sh new file mode 100755 index 0000000..745f68a --- /dev/null +++ b/tools/init.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +chmod -f 644 /data/*.rdb /data/*.aof /data/*.conf > /dev/null || true +chown -f 999:1000 /data/*.rdb /data/*.aof /data/*.conf > /dev/null || true + + +if [ "$SERVICE_TYPE" = "LoadBalancer" ] || [ "$SERVICE_TYPE" = "NodePort" ] || [ -n "$IP_FAMILY_PREFER" ] ; then + echo "check pod binded service" + /opt/redis-tools cluster expose || exit 1 +fi + +# copy binaries +cp /opt/* /mnt/opt/ && chmod 555 /mnt/opt/* diff --git a/tools/init_failover.sh b/tools/init_failover.sh new file mode 100755 index 0000000..babcd3b --- /dev/null +++ b/tools/init_failover.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +chmod -f 644 /data/*.rdb /data/*.aof 2>/dev/null || true +chown -f 999:1000 /data/*.rdb /data/*.aof 2>/dev/null || true + + +if [ "${SERVICE_TYPE}" = "LoadBalancer" ] || [ "${SERVICE_TYPE}" = "NodePort" ] || [ -n "${IP_FAMILY_PREFER}" ] ; then + echo "check pod binded service" + /opt/redis-tools failover expose || exit 1 +fi + +# copy binaries +cp -r /opt/* /mnt/opt/ && chmod 555 /mnt/opt/* diff --git a/tools/init_sentinel.sh b/tools/init_sentinel.sh new file mode 100755 index 0000000..6efa969 --- /dev/null +++ b/tools/init_sentinel.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ "$SERVICE_TYPE" = "LoadBalancer" ] || [ "$SERVICE_TYPE" = "NodePort" ] || [ -n "$IP_FAMILY_PREFER" ] ; then + echo "check pod binded service" + /opt/redis-tools sentinel expose || exit 1 +fi + +# copy binaries +cp -r /opt/* /mnt/opt/ && chmod 555 /mnt/opt/* diff --git a/tools/run.sh b/tools/run.sh new file mode 100755 index 0000000..5df03b3 --- /dev/null +++ b/tools/run.sh @@ -0,0 +1,89 @@ +#!/bin/sh + +export PATH=/opt:$PATH + +REDIS_CONFIG="/tmp/redis.conf" +ACL_CONFIG="/tmp/acl.conf" +ANNOUNCE_CONFIG="/data/announce.conf" +CLUSTER_CONFIG="/data/nodes.conf" +OPERATOR_PASSWORD_FILE="/account/password" +TLS_DIR="/tls" +ACL_ARGS="" + +echo "# Run: cluster heal" +/opt/redis-tools cluster heal || exit 1 + +LISTEN=${POD_IP} +LOCALHOST="127.0.0.1" +if echo "${POD_IP}" | grep -q ':'; then + LOCALHOST="::1" +fi + +if echo "${POD_IPS}" | grep -q ','; then + POD_IPS_LIST=$(echo "${POD_IPS}" | tr ',' ' ') + for ip in $POD_IPS_LIST; do + if [ "$IP_FAMILY_PREFER" = "IPv6" ]; then + if echo "$ip" | grep -q ':'; then + LISTEN="$ip" + LOCALHOST="::1" + break + fi + elif [ "$IP_FAMILY_PREFER" = "IPv4" ]; then + if echo "$ip" | grep -q '\.'; then + LISTEN="$ip" + LOCALHOST="127.0.0.1" + break + fi + fi + done +fi + +if [ -f ${CLUSTER_CONFIG} ]; then + if [ -z "${LISTEN}" ]; then + echo "Unable to determine Pod IP address!" + exit 1 + fi + sed -i.bak -e "/myself/ s/ .*:[0-9]*@[0-9]*/ ${LISTEN}:6379@16379/" ${CLUSTER_CONFIG} +fi + +cat /conf/redis.conf > ${REDIS_CONFIG} + +password=$(cat ${OPERATOR_PASSWORD_FILE} 2>/dev/null) + +# when redis acl supported, inject acl config +if [ -n "${ACL_CONFIGMAP_NAME}" ]; then + echo "# Run: generate acl" + /opt/redis-tools helper generate acl --name ${ACL_CONFIGMAP_NAME} --namespace ${NAMESPACE} > ${ACL_CONFIG} || exit 1 + ACL_ARGS="--aclfile ${ACL_CONFIG}" +fi + +if [ "${ACL_ENABLED}" = "true" ]; then + if [ -n "${OPERATOR_USERNAME}" ]; then + echo "masteruser \"${OPERATOR_USERNAME}\"" >> ${REDIS_CONFIG} + fi + if [ -n "${password}" ]; then + echo "masterauth \"${password}\"" >> ${REDIS_CONFIG} + fi +elif [ -n "${password}" ]; then + echo "masterauth \"${password}\"" >> ${REDIS_CONFIG} + echo "requirepass \"${password}\"" >> ${REDIS_CONFIG} +fi + +if [ -f ${ANNOUNCE_CONFIG} ]; then + echo "append announce conf to redis config" + cat ${ANNOUNCE_CONFIG} >> ${REDIS_CONFIG} +fi + +if [ "${LISTEN}" != "${POD_IP}" ]; then + LISTEN="${LISTEN} ${POD_IP}" +fi +ARGS="--cluster-enabled yes --cluster-config-file ${CLUSTER_CONFIG} --protected-mode no --bind ${LISTEN} ${LOCALHOST}" + +if [ "${TLS_ENABLED}" = "true" ]; then + ARGS="${ARGS} --port 0 --tls-port 6379 --tls-cluster yes --tls-replication yes --tls-cert-file ${TLS_DIR}/tls.crt --tls-key-file ${TLS_DIR}/tls.key --tls-ca-cert-file ${TLS_DIR}/ca.crt" +fi + +chmod 0600 ${REDIS_CONFIG} +chmod 0600 ${ACL_CONFIG} + +redis-server ${REDIS_CONFIG} ${ACL_ARGS} ${ARGS} $@ diff --git a/tools/run_failover.sh b/tools/run_failover.sh new file mode 100755 index 0000000..3f1f3e0 --- /dev/null +++ b/tools/run_failover.sh @@ -0,0 +1,123 @@ +#!/bin/sh + +export PATH=/opt:$PATH + +# Read Redis credentials +REDIS_PASSWORD=$(cat /account/password 2>/dev/null) +REDIS_USERNAME=$(cat /account/username 2>/dev/null) +ACL_ARGS="" +ACL_CONFIG="/tmp/acl.conf" + +# Copy base Redis configuration +CONFIG_FILE="/tmp/redis.conf" +cat /redis/redis.conf > "$CONFIG_FILE" + +# Append username to Redis configuration if it exists +if [ -n "$REDIS_USERNAME" ]; then + echo "masteruser \"$REDIS_USERNAME\"" >> "$CONFIG_FILE" +fi + +# Check for new password file and update Redis password if it exists +if [ -e /tmp/newpass ]; then + echo "## new passwd found" + REDIS_PASSWORD=$(cat /tmp/newpass) +fi + +# Append password to Redis configuration if it exists +if [ -n "$REDIS_PASSWORD" ]; then + echo "requirepass \"$REDIS_PASSWORD\"" >> "$CONFIG_FILE" + echo "masterauth \"$REDIS_PASSWORD\"" >> "$CONFIG_FILE" +fi + +# Generate ACL configuration if ACL_CONFIGMAP_NAME is set +if [ -n "$ACL_CONFIGMAP_NAME" ]; then + echo "## generate acl" + /opt/redis-tools helper generate acl --name "$ACL_CONFIGMAP_NAME" > "$ACL_CONFIG" || exit 1 + ACL_ARGS="--aclfile $ACL_CONFIG" +fi + +# Handle sentinel monitoring policy +if [ "$MONITOR_POLICY" = "sentinel" ]; then + ANNOUNCE_CONFIG="/data/announce.conf" + ANNOUNCE_IP="" + ANNOUNCE_PORT="" + if [ -f "$ANNOUNCE_CONFIG" ]; then + echo "" >> "$CONFIG_FILE" + cat "$ANNOUNCE_CONFIG" >> "$CONFIG_FILE" + + ANNOUNCE_IP=$(grep 'announce-ip' "$ANNOUNCE_CONFIG" | awk '{print $2}') + ANNOUNCE_PORT=$(grep 'announce-port' "$ANNOUNCE_CONFIG" | awk '{print $2}') + fi + + echo "## check and do failover" + /opt/redis-tools sentinel failover --escape "${ANNOUNCE_IP}:${ANNOUNCE_PORT}" --escape "[${ANNOUNCE_IP}]:${ANNOUNCE_PORT}" --timeout 120 + # Get current master info + addr=$(/opt/redis-tools sentinel get-master-addr --healthy) + if [ $? -eq 0 ] && [ -n "$addr" ]; then + # Check if the address is IPv6 or IPv4 + if echo "$addr" | grep -q ']:'; then + master=$(echo "$addr" | sed -n 's/\(\[.*\]\):\([0-9]*\)/\1/p' | tr -d '[]') + masterPort=$(echo "$addr" | sed -n 's/\(\[.*\]\):\([0-9]*\)/\2/p') + else + master=$(echo "$addr" | cut -d ':' -f 1) + masterPort=$(echo "$addr" | cut -d ':' -f 2) + fi + + echo "## current master: $addr" + if [ "$master" != "127.0.0.1" ] && [ "$master" != "::1" ]; then + if [ "$masterPort" != "6379" ] && [ "$masterPort" != "$ANNOUNCE_PORT" ]; then + echo "## config $master $masterPort as my master" + echo "" >> "$CONFIG_FILE" + echo "slaveof $master $masterPort" >> "$CONFIG_FILE" + elif [ "$masterPort" != "$ANNOUNCE_PORT" ] && [ "$master" != "$ANNOUNCE_IP" ]; then + echo "## config $master $masterPort as my master" + echo "" >> "$CONFIG_FILE" + echo "slaveof $master $masterPort" >> "$CONFIG_FILE" + fi + fi + fi +fi + +# Determine localhost based on IP family preference +LISTEN=${POD_IP} +LOCALHOST="127.0.0.1" +if echo "${POD_IP}" | grep -q ':'; then + LOCALHOST="::1" +fi + +if echo "${POD_IPS}" | grep -q ','; then + POD_IPS_LIST=$(echo "${POD_IPS}" | tr ',' ' ') + for ip in $POD_IPS_LIST; do + if [ "$IP_FAMILY_PREFER" = "IPv6" ]; then + if echo "$ip" | grep -q ':'; then + LISTEN="$ip" + LOCALHOST="::1" + break + fi + elif [ "$IP_FAMILY_PREFER" = "IPv4" ]; then + if echo "$ip" | grep -q '\.'; then + LISTEN="$ip" + LOCALHOST="127.0.0.1" + break + fi + fi + done +fi + +if [ "${LISTEN}" != "${POD_IP}" ]; then + LISTEN="${LISTEN} ${POD_IP}" +fi +# Listne only to protocol matched IP +ARGS="--protected-mode no --bind $LISTEN $LOCALHOST" + +# Add TLS arguments if TLS is enabled +if [ "$TLS_ENABLED" = "true" ]; then + ARGS="$ARGS --port 0 --tls-port 6379 --tls-replication yes --tls-cert-file $TLS_DIR/tls.crt --tls-key-file $TLS_DIR/tls.key --tls-ca-cert-file $TLS_DIR/ca.crt" +fi + +# Set permissions for configuration files +chmod 0600 "$CONFIG_FILE" +chmod 0600 "$ACL_CONFIG" + +# Start Redis server with the constructed arguments +redis-server "$CONFIG_FILE" $ACL_ARGS $ARGS $@ diff --git a/tools/run_sentinel.sh b/tools/run_sentinel.sh new file mode 100755 index 0000000..28dfbc6 --- /dev/null +++ b/tools/run_sentinel.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +export PATH=/opt:$PATH + +RAW_SENTINEL_CONFIG="/conf/sentinel.conf" +SENTINEL_CONFIG="/data/sentinel.conf" +ANNOUNCE_CONFIG="/data/announce.conf" +OPERATOR_PASSWORD_FILE="/account/password" +TLS_DIR="/tls" + +cat ${RAW_SENTINEL_CONFIG} > ${SENTINEL_CONFIG} + +# Append password to sentinel configuration if it exists +password=$(cat ${OPERATOR_PASSWORD_FILE} 2>/dev/null) +if [ -n "${password}" ]; then + echo "requirepass \"${password}\"" >> ${SENTINEL_CONFIG} +fi + +# Append announce configuration to sentinel configuration if it exists +if [ -f ${ANNOUNCE_CONFIG} ]; then + echo "# append announce conf to sentinel config" + cat "${ANNOUNCE_CONFIG}" | grep "announce" | sed "s/^/sentinel /" >> ${SENTINEL_CONFIG} +fi + +# Merge custom configuration +/opt/redis-tools sentinel merge-config --local-conf-file "${SENTINEL_CONFIG}" + +# Determine localhost based on IP family preference +LISTEN=${POD_IP} +LOCALHOST="127.0.0.1" +if echo "${POD_IP}" | grep -q ':'; then + LOCALHOST="::1" +fi + +if echo "${POD_IPS}" | grep -q ','; then + POD_IPS_LIST=$(echo "${POD_IPS}" | tr ',' ' ') + for ip in $POD_IPS_LIST; do + if [ "$IP_FAMILY_PREFER" = "IPv6" ]; then + if echo "$ip" | grep -q ':'; then + LISTEN="$ip" + LOCALHOST="::1" + break + fi + elif [ "$IP_FAMILY_PREFER" = "IPv4" ]; then + if echo "$ip" | grep -q '\.'; then + LISTEN="$ip" + LOCALHOST="127.0.0.1" + break + fi + fi + done +fi + +if [ "${LISTEN}" != "${POD_IP}" ]; then + LISTEN="${LISTEN} ${POD_IP}" +fi +# Construct arguments for redis-server +ARGS="--sentinel --protected-mode no --bind ${LISTEN} ${LOCALHOST}" + +# Add TLS arguments if TLS is enabled +if [ "${TLS_ENABLED}" = "true" ]; then + ARGS="${ARGS} --port 0 --tls-port 26379 --tls-replication yes --tls-cert-file ${TLS_DIR}/tls.crt --tls-key-file ${TLS_DIR}/tls.key --tls-ca-cert-file ${TLS_DIR}/ca.crt" +else + ARGS="${ARGS} --port 26379" +fi + +# Set permissions for sentinel configuration +chmod 0600 ${SENTINEL_CONFIG} + +# Start redis-server with the constructed arguments +redis-server ${SENTINEL_CONFIG} ${ARGS} $@ | sed 's/auth-pass .*/auth-pass ******/' diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..6633273 --- /dev/null +++ b/values.yaml @@ -0,0 +1,24 @@ +global: + operatorVersion: 3.18.0 + images: + - name: DEFAULT_REDIS_IMAGE + image: redis + tag: "6.0" + - name: REDIS_TOOLS_IMAGE + image: redis-tools + tag: "" + - name: DEFAULT_EXPORTER_IMAGE + image: oliver006/redis_exporter + tag: v1.35.1 + - name: REDIS_VERSION_6_IMAGE + image: redis + tag: "6.0" + - name: REDIS_VERSION_6_2_IMAGE + image: redis + tag: "6.2" + - name: REDIS_VERSION_7_2_IMAGE + image: redis + tag: "7.2" + - name: REDIS_VERSION_7_4_IMAGE + image: redis + tag: "7.4" diff --git a/version b/version new file mode 100644 index 0000000..c5b45eb --- /dev/null +++ b/version @@ -0,0 +1 @@ +3.18.0