├── .asf.yaml ├── .github ├── licenserc.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── VERSION.txt ├── cmd ├── client │ ├── command │ │ ├── client.go │ │ ├── consts.go │ │ ├── create.go │ │ ├── delete.go │ │ ├── failover.go │ │ ├── get.go │ │ ├── helper.go │ │ ├── import.go │ │ ├── list.go │ │ ├── migrate.go │ │ └── raft.go │ └── main.go └── server │ └── main.go ├── config ├── config-consul.yaml ├── config-raft.yaml ├── config-zk.yaml ├── config.go ├── config.yaml └── config_test.go ├── consts ├── context_key.go └── errors.go ├── controller ├── cluster.go ├── cluster_test.go ├── controller.go └── controller_test.go ├── docs ├── API.md └── images │ ├── overview.png │ └── server.gif ├── go.mod ├── go.sum ├── licenses ├── LICENSE-atomic.txt ├── LICENSE-consul.txt ├── LICENSE-gin.txt ├── LICENSE-go-redis.txt ├── LICENSE-resty.txt ├── LICENSE-tablewriter.txt ├── LICENSE-testify.txt ├── LICENSE-validator.txt ├── LICENSE-zap.txt └── LICENSE-zk.txt ├── logger └── logger.go ├── metrics └── setup.go ├── scripts ├── build.sh ├── docker │ ├── docker-compose.yml │ ├── kvrocks │ │ └── Dockerfile │ ├── pg-dockerfile │ │ └── Dockerfile │ └── pg-init-scripts │ │ └── init.sql ├── run-test.sh ├── setup.sh └── teardown.sh ├── server ├── api │ ├── cluster.go │ ├── cluster_test.go │ ├── handler.go │ ├── namespace.go │ ├── namespace_test.go │ ├── node.go │ ├── node_test.go │ ├── raft.go │ ├── shard.go │ └── shard_test.go ├── helper │ ├── helper.go │ └── helper_test.go ├── middleware │ └── middleware.go ├── route.go └── server.go ├── store ├── cluster.go ├── cluster_mock_node.go ├── cluster_node.go ├── cluster_node_test.go ├── cluster_shard.go ├── cluster_shard_test.go ├── cluster_test.go ├── engine │ ├── consul │ │ ├── consul.go │ │ └── consul_test.go │ ├── engine.go │ ├── engine_inmemory.go │ ├── etcd │ │ ├── etcd.go │ │ └── etcd_test.go │ ├── postgresql │ │ ├── postgresql.go │ │ └── postgresql_test.go │ ├── raft │ │ ├── config.go │ │ ├── config_test.go │ │ ├── logger.go │ │ ├── node.go │ │ ├── node_test.go │ │ ├── store.go │ │ └── store_test.go │ └── zookeeper │ │ ├── zookeeper.go │ │ └── zookeeper_test.go ├── event.go ├── helper.go ├── slot.go ├── slot_test.go ├── store.go └── store_test.go ├── util ├── network.go ├── slot.go ├── string.go └── string_test.go ├── version └── version.go ├── webui ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── config.ts ├── next.config.mjs ├── package.json ├── postcss.config.js ├── public │ ├── asf_logo.svg │ └── logo.svg ├── src │ └── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── lib │ │ ├── api.ts │ │ └── definitions.ts │ │ ├── namespaces │ │ ├── [namespace] │ │ │ ├── clusters │ │ │ │ └── [cluster] │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── shards │ │ │ │ │ └── [shard] │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── nodes │ │ │ │ │ └── [node] │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── theme-provider.tsx │ │ ├── ui │ │ ├── banner.tsx │ │ ├── createCard.tsx │ │ ├── emptyState.tsx │ │ ├── footer.tsx │ │ ├── formCreation.tsx │ │ ├── formDialog.tsx │ │ ├── loadingSpinner.tsx │ │ ├── nav-links.tsx │ │ ├── sidebar.tsx │ │ └── sidebarItem.tsx │ │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json └── x.py /.asf.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | # For more information, see https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features. 19 | 20 | github: 21 | description: >- 22 | Apache Kvrocks Controller is a cluster management tool for Apache Kvrocks. 23 | homepage: https://kvrocks.apache.org/ 24 | labels: 25 | - kvrocks 26 | - controller 27 | - cluster 28 | - distributed 29 | - database 30 | - redis 31 | - redis-cluster 32 | enabled_merge_buttons: 33 | squash: true 34 | merge: false 35 | rebase: true 36 | protected_branches: 37 | unstable: 38 | required_pull_request_reviews: 39 | dismiss_stale_reviews: false 40 | required_approving_review_count: 1 41 | 42 | notifications: 43 | commits: commits@kvrocks.apache.org 44 | issues: issues@kvrocks.apache.org 45 | pullrequests: issues@kvrocks.apache.org 46 | jobs: builds@kvrocks.apache.org 47 | discussions: issues@kvrocks.apache.org 48 | -------------------------------------------------------------------------------- /.github/licenserc.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | header: 19 | license: 20 | spdx-id: Apache-2.0 21 | copyright-owner: Apache Software Foundation 22 | paths: 23 | - '**/*.go' 24 | - '**/*.sh' 25 | - '**/*.yml' 26 | - '**/*.yaml' 27 | paths-ignore: 28 | - '**/go.mod' 29 | - '**/go.sum' 30 | - '**/*.json' -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | name: CI Actions # don't edit while the badge was depend on this 19 | 20 | on: [push, pull_request] 21 | 22 | jobs: 23 | license-check: 24 | name: License check 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout Code Base 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - uses: apache/skywalking-eyes/header@v0.6.0 33 | with: 34 | config: .github/licenserc.yaml 35 | lint: 36 | name: Lint 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Install Go 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version: 1.23 43 | 44 | - name: Checkout Code Base 45 | uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | 49 | - name: Restore Go Module Cache 50 | uses: actions/cache@v4 51 | with: 52 | path: ~/go/pkg/mod 53 | key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} 54 | restore-keys: | 55 | ${{ runner.os }}-go-${{ matrix.go-version }}- 56 | 57 | - name: Make Lint 58 | run: | 59 | export GOPATH=$HOME/go 60 | export PATH=$PATH:$GOPATH/bin 61 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.63.4 62 | make lint 63 | 64 | - name: Install Prettier in webui 65 | run: cd webui && npm install --save-dev prettier 66 | 67 | - name: Run Prettier Lint in webui 68 | run: cd webui && npx prettier --check . 69 | 70 | build-test: 71 | name: Build and test 72 | needs: [license-check, lint] 73 | strategy: 74 | matrix: 75 | go-version: [1.23, stable] 76 | os: [ubuntu-latest] 77 | runs-on: ${{ matrix.os }} 78 | steps: 79 | - name: Install Go 80 | uses: actions/setup-go@v5 81 | with: 82 | go-version: ${{matrix.go-version}} 83 | 84 | - name: Checkout Code Base 85 | uses: actions/checkout@v4 86 | with: 87 | fetch-depth: 0 88 | 89 | - name: Restore Go Module Cache 90 | uses: actions/cache@v4 91 | with: 92 | path: ~/go/pkg/mod 93 | key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} 94 | restore-keys: | 95 | ${{ runner.os }}-go-${{ matrix.go-version }}- 96 | 97 | - name: Build 98 | run: make 99 | 100 | - name: Build Web UI 101 | run: cd webui && npm install && npm run build 102 | 103 | - name: Make Test 104 | run: make test 105 | 106 | - name: Upload Coverage Report 107 | uses: codecov/codecov-action@v5 108 | with: 109 | file: ./coverage.out 110 | flags: unittests 111 | name: codecov-umbrella 112 | 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | # Binaries for programs and plugins 19 | *.exe 20 | *.exe~ 21 | *.dll 22 | *.so 23 | *.dylib 24 | 25 | # Test binary, built with `go test -c` 26 | *.test 27 | 28 | # Output of the go coverage tool, specifically when used with LiteIDE 29 | *.out 30 | 31 | # Dependency directories (remove the comment below to include it) 32 | # vendor/ 33 | .idea 34 | .swo 35 | .swp 36 | _build 37 | coverage.* 38 | cmd/cli/cli 39 | cmd/server/kvrocks_controller 40 | .kc_cli_history 41 | .vscode/ 42 | vendor 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | FROM golang:1.23 as build 19 | 20 | WORKDIR /kvctl 21 | 22 | # If you encounter some issues when pulling modules, \ 23 | # you can try to use GOPROXY, especially in China. 24 | # ENV GOPROXY=https://goproxy.cn 25 | 26 | COPY . . 27 | RUN make 28 | 29 | 30 | FROM ubuntu:focal 31 | 32 | WORKDIR /kvctl 33 | 34 | COPY --from=build /kvctl/_build/kvctl-server ./bin/ 35 | COPY --from=build /kvctl/_build/kvctl ./bin/ 36 | 37 | VOLUME /var/lib/kvctl 38 | 39 | COPY ./LICENSE ./NOTICE ./licenses ./ 40 | COPY ./config/config.yaml /var/lib/kvctl/ 41 | 42 | EXPOSE 9379:9379 43 | ENTRYPOINT ["./bin/kvctl-server", "-c", "/var/lib/kvctl/config.yaml"] 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | PROGRAM=kvrocks-controller 19 | 20 | CCCOLOR="\033[37;1m" 21 | MAKECOLOR="\033[32;1m" 22 | ENDCOLOR="\033[0m" 23 | 24 | all: $(PROGRAM) 25 | 26 | .PHONY: all 27 | 28 | 29 | $(PROGRAM): 30 | @bash scripts/build.sh 31 | @echo "" 32 | @printf $(MAKECOLOR)"Hint: It's a good idea to run 'make test' ;)"$(ENDCOLOR) 33 | @echo "" 34 | 35 | setup: 36 | @cd scripts && sh setup.sh && cd .. 37 | 38 | teardown: 39 | @cd scripts && sh teardown.sh && cd .. 40 | 41 | test: 42 | @cd scripts && sh setup.sh && cd .. 43 | @scripts/run-test.sh 44 | @cd scripts && sh teardown.sh && cd .. 45 | 46 | lint: 47 | @printf $(CCCOLOR)"GolangCI Lint...\n"$(ENDCOLOR) 48 | @golangci-lint run 49 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache Kvrocks Controller 2 | Copyright 2023-2025 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | 7 | ================================================================ 8 | 9 | This product includes a number of Dependencies with separate copyright notices 10 | and license terms. Your use of these submodules is subject to the terms and 11 | conditions of the following licenses. 12 | 13 | ================================================================ 14 | 15 | ================================================================ 16 | Apache-2.0 licenses 17 | ================================================================ 18 | 19 | The following components are provided under the Apache-2.0 License.See project link for details. 20 | The text of each license is the standard Apache 2.0 license. 21 | 22 | - github.com/prometheus/client_golang 23 | - github.com/spf13/cobra 24 | - go.etcd.io/etcd 25 | - gopkg.in/yaml.v1 26 | 27 | ================================================================ 28 | BSD-2-Clause licenses 29 | ================================================================ 30 | The following components are provided under the BSD-2-Clause License. See project link for details. 31 | The text of each license is also included in licenses/LICENSE-[project].txt. 32 | 33 | - github.com/redis/go-redis 34 | 35 | ================================================================ 36 | BSD-3-Clause licenses 37 | ================================================================ 38 | The following components are provided under the BSD-3-Clause License. See project link for details. 39 | The text of each license is also included in licenses/LICENSE-[project].txt. 40 | 41 | - github.com/go-zookeeper/zk 42 | 43 | ================================================================ 44 | MIT licenses 45 | ================================================================ 46 | The following components are provided under the MIT License. See project link for details. 47 | The text of each license is also included in licenses/LICENSE-[project].txt 48 | 49 | - github.com/gin-gonic/gin 50 | - github.com/olekukonko/tablewriter 51 | - github.com/stretchr/testify 52 | - go.uber.org/atomic 53 | - go.uber.org/zap 54 | - github.com/go-playground/validator 55 | - github.com/go-resty/resty 56 | 57 | ================================================================ 58 | MPL-2.0 licenses 59 | ================================================================ 60 | 61 | The following components are provided under the MPL 2.0 License. See project link for details. 62 | The text of each license is also included in licenses/LICENSE-[project].txt 63 | 64 | - github.com/hashicorp/consul/api 65 | 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apache Kvrocks Controller 2 | 3 | [![Build Status](https://github.com/apache/kvrocks-controller/workflows/CI%20Actions/badge.svg)](https://github.com/apache/kvrocks-controller/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/apache/kvrocks-controller)](https://goreportcard.com/report/github.com/apache/kvrocks-controller) [![codecov](https://codecov.io/gh/apache/kvrocks-controller/branch/unsteable/graph/badge.svg?token=EKU6KU5IWK)](https://codecov.io/gh/apache/kvrocks-controller) 4 | 5 | Apache Kvrocks Controller is a cluster management tool for [Apache Kvrocks](https://github.com/apache/incubator-kvrocks), including the following key features: 6 | 7 | * Failover - controller will failover or remove the master/slave node when probing failed 8 | * Scale out the cluster in one line command 9 | * Manage many clusters in one controller cluster 10 | * Support multi metadata storages like etcd and so on 11 | 12 | ## Building and Running 13 | 14 | ### Build binaries 15 | 16 | ```shell 17 | $ git clone https://github.com/apache/kvrocks-controller 18 | $ cd kvrocks-controller 19 | $ make # You can find the binary file in the `_build` dir if all goes good 20 | ``` 21 | 22 | ### Overview 23 | ![image](docs/images/overview.png) 24 | For the storage, the ETCD is used as the default storage now. Welcome to contribute other storages like MySQL, Redis, Consul and so on. And what you need to do is to implement the [Engine interface](https://github.com/apache/kvrocks-controller/blob/unstable/store/engine/engine.go). 25 | 26 | ### Supported Storage Engine 27 | - ETCD 28 | - Zookeeper 29 | - Consul by HashiCorp 30 | - Embedded Raft storage (experimental) 31 | 32 | ### Run the controller server 33 | 34 | ```shell 35 | # Use docker-compose to setup the etcd or zookeeper 36 | $ make setup 37 | # Run the controller server 38 | $ ./_build/kvctl-server -c config/config.yaml 39 | ``` 40 | 41 | ### Run the controller server in Docker 42 | 43 | ```shell 44 | $ docker run -it -p 9379:9379 apache/kvrocks-controller:latest 45 | ``` 46 | 47 | ![image](docs/images/server.gif) 48 | 49 | ### Run server with the embedded Raft engine 50 | 51 | > Note: The embedded Raft engine is still in the experimental stage, and it's not recommended to use it in the production environment. 52 | 53 | Change the storage type to `raft` in the configuration file. 54 | 55 | ```yaml 56 | storage_type: raft 57 | 58 | raft: 59 | id: 1 60 | data_dir: "/data/kvrocks/raft/1" 61 | cluster_state: "new" 62 | peers: 63 | - "http://127.0.0.1:6001" 64 | - "http://127.0.0.1:6002" 65 | - "http://127.0.0.1:6003" 66 | ``` 67 | 68 | - `id`: the id for the raft node, it's also an index in the `peers` list 69 | - `data_dir`: the directory to store the raft data 70 | - `cluster_state`: the state of the raft cluster, it should be `new` when the cluster is initialized. And it should be `existing` when the cluster is already bootstrapped. 71 | - `peers`: the list of the raft peers, it should include all the nodes in the cluster. 72 | 73 | And then you can run the controller server with the configuration file. 74 | 75 | ```shell 76 | $ ./_build/kvctl-server -c config/config-raft.yaml 77 | ``` 78 | 79 | #### Add/Remove a raft peer node 80 | 81 | We now support adding and removing via the HTTP API. 82 | 83 | ```shell 84 | # Add a new peer node 85 | curl -XPOST -d '{"id":4,"peer":"http://127.0.0.1:6004","operation":"add"}' http://127.0.0.1:9379/api/v1/raft/peers 86 | 87 | # Remove a peer node 88 | curl -XPOST -d '{"id":4, "operation":"remove"}' http://127.0.0.1:9379/api/v1/raft/peers 89 | 90 | # List all the peer nodes 91 | curl http://127.0.0.1:9379/api/v1/raft/peers 92 | ``` 93 | 94 | ### Use client to interact with the controller server 95 | 96 | ```shell 97 | # Show help 98 | $ ./_build/kvctl --help 99 | 100 | # Create namespace 101 | $ ./_build/kvctl create namespace test-ns 102 | 103 | # List namespaces 104 | $ ./_build/kvctl list namespaces 105 | 106 | # Create cluster in the namespace 107 | $ ./_build/kvctl create cluster test-cluster --nodes 127.0.0.1:6666,127.0.0.1:6667 -n test-ns 108 | 109 | # List clusters in the namespace 110 | $ ./_build/kvctl list clusters -n test-ns 111 | 112 | # Get cluster in the namespace 113 | $ ./_build/kvctl get cluster test-cluster -n test-ns 114 | 115 | # Migrate slot from source to target 116 | $ ./_build/kvctl migrate slot 123 --target 1 -n test-ns -c test-cluster 117 | ``` 118 | 119 | For the HTTP API, you can find the [HTTP API(work in progress)](docs/API.md) for more details. 120 | 121 | ## License 122 | 123 | Licensed under the [Apache License, Version 2.0](LICENSE) 124 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | unstable -------------------------------------------------------------------------------- /cmd/client/command/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | import ( 24 | "encoding/json" 25 | "errors" 26 | "reflect" 27 | 28 | "github.com/go-resty/resty/v2" 29 | ) 30 | 31 | const ( 32 | apiVersionV1 = "/api/v1" 33 | 34 | defaultHost = "http://127.0.0.1:9379" 35 | ) 36 | 37 | type client struct { 38 | restyCli *resty.Client 39 | host string 40 | } 41 | 42 | type ErrorMessage struct { 43 | Message string `json:"message"` 44 | } 45 | 46 | type response struct { 47 | Error *ErrorMessage `json:"error"` 48 | Data any `json:"data"` 49 | } 50 | 51 | func newClient(host string) *client { 52 | if host == "" { 53 | host = defaultHost 54 | } 55 | restyCli := resty.New().SetBaseURL(host + apiVersionV1) 56 | return &client{ 57 | restyCli: restyCli, 58 | host: host, 59 | } 60 | } 61 | 62 | func unmarshalData(body []byte, v any) error { 63 | if len(body) == 0 { 64 | return errors.New("empty response body") 65 | } 66 | 67 | rv := reflect.ValueOf(v) 68 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 69 | return errors.New("unmarshal receiver was non-pointer") 70 | } 71 | 72 | var rsp response 73 | rsp.Data = v 74 | return json.Unmarshal(body, &rsp) 75 | } 76 | 77 | func unmarshalError(body []byte) error { 78 | var rsp response 79 | if err := json.Unmarshal(body, &rsp); err != nil { 80 | return err 81 | } 82 | if rsp.Error != nil { 83 | return errors.New(rsp.Error.Message) 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /cmd/client/command/consts.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | const ( 24 | ResourceNamespace = "namespace" 25 | ResourceCluster = "cluster" 26 | ResourceShard = "shard" 27 | ResourceNode = "node" 28 | ) 29 | -------------------------------------------------------------------------------- /cmd/client/command/failover.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | import ( 24 | "fmt" 25 | "strconv" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | type FailoverOptions struct { 31 | namespace string 32 | cluster string 33 | preferred string 34 | } 35 | 36 | var failoverOptions FailoverOptions 37 | 38 | var FailoverCommand = &cobra.Command{ 39 | Use: "failover", 40 | Short: "Failover the master of a shard", 41 | Example: ` 42 | # Failover the master of a shard 43 | kvctl failover shard -n -c 44 | 45 | # Failover the master of a shard with preferred slave 46 | kvctl failover shard --preferred -n -c 47 | `, 48 | PreRunE: failoverPreRun, 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | if len(args) < 2 { 51 | return fmt.Errorf("missing shard index, plese specify the shard index") 52 | } 53 | host, _ := cmd.Flags().GetString("host") 54 | client := newClient(host) 55 | resource := args[0] 56 | switch resource { 57 | case "shard": 58 | shardIndex, err := strconv.Atoi(args[1]) 59 | if err != nil { 60 | return fmt.Errorf("invalid shard index: %s", args[1]) 61 | } 62 | return failoverShard(client, &failoverOptions, shardIndex) 63 | default: 64 | return fmt.Errorf("unsupported resource type: %s", resource) 65 | } 66 | }, 67 | } 68 | 69 | func failoverPreRun(cmd *cobra.Command, args []string) error { 70 | if len(args) < 1 { 71 | return fmt.Errorf("missing resource name, must be [shard]") 72 | } 73 | if failoverOptions.namespace == "" { 74 | return fmt.Errorf("namespace is required") 75 | } 76 | if failoverOptions.cluster == "" { 77 | return fmt.Errorf("cluster is required") 78 | } 79 | return nil 80 | } 81 | 82 | func failoverShard(client *client, options *FailoverOptions, shardIndex int) error { 83 | rsp, err := client.restyCli.R(). 84 | SetPathParam("namespace", options.namespace). 85 | SetPathParam("cluster", options.cluster). 86 | SetPathParam("shard", strconv.Itoa(shardIndex)). 87 | Post("/namespaces/{namespace}/clusters/{cluster}/shards/{shard}/failover") 88 | if err != nil { 89 | return err 90 | } 91 | if rsp.IsError() { 92 | return unmarshalError(rsp.Body()) 93 | } 94 | var result struct { 95 | NewMasterID string `json:"new_master_id"` 96 | } 97 | if err := unmarshalData(rsp.Body(), &result); err != nil { 98 | return err 99 | } 100 | printLine("failover shard %d successfully, new master id: %s.", shardIndex, result.NewMasterID) 101 | return nil 102 | } 103 | 104 | func init() { 105 | FailoverCommand.Flags().StringVarP(&failoverOptions.namespace, "namespace", "n", "", "The namespace of the cluster") 106 | FailoverCommand.Flags().StringVarP(&failoverOptions.cluster, "cluster", "c", "", "The name of the cluster") 107 | FailoverCommand.Flags().StringVarP(&failoverOptions.preferred, "preferred", "", "", "The preferred slave node id") 108 | } 109 | -------------------------------------------------------------------------------- /cmd/client/command/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/spf13/cobra" 28 | 29 | "github.com/apache/kvrocks-controller/store" 30 | ) 31 | 32 | type GetOptions struct { 33 | namespace string 34 | cluster string 35 | } 36 | 37 | var getOptions GetOptions 38 | 39 | var GetCommand = &cobra.Command{ 40 | Use: "get", 41 | Short: "Get a resource", 42 | Example: ` 43 | # Get a cluster 44 | kvctl get cluster -n 45 | `, 46 | PreRunE: getPreRun, 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | host, _ := cmd.Flags().GetString("host") 49 | client := newClient(host) 50 | if len(args) < 2 { 51 | return fmt.Errorf("missing resource name, must be one of [cluster, shard]") 52 | } 53 | resource := strings.ToLower(args[0]) 54 | switch resource { 55 | case "cluster": 56 | getOptions.cluster = args[1] 57 | return getCluster(client, &getOptions) 58 | default: 59 | return fmt.Errorf("unsupported resource type %s", resource) 60 | } 61 | }, 62 | SilenceUsage: true, 63 | SilenceErrors: true, 64 | } 65 | 66 | func getPreRun(_ *cobra.Command, args []string) error { 67 | if len(args) == 0 { 68 | return fmt.Errorf("missing resource type") 69 | } 70 | 71 | if getOptions.namespace == "" { 72 | return fmt.Errorf("missing namespace, please specify the namespace via -n or --namespace option") 73 | } 74 | return nil 75 | } 76 | 77 | func getCluster(client *client, options *GetOptions) error { 78 | rsp, err := client.restyCli.R().SetPathParams(map[string]string{ 79 | "namespace": options.namespace, 80 | "cluster": options.cluster, 81 | }).Get("/namespaces/{namespace}/clusters/{cluster}") 82 | if err != nil { 83 | return err 84 | } 85 | if rsp.IsError() { 86 | return unmarshalError(rsp.Body()) 87 | } 88 | 89 | var result struct { 90 | Cluster *store.Cluster `json:"cluster"` 91 | } 92 | if err := unmarshalData(rsp.Body(), &result); err != nil { 93 | return err 94 | } 95 | printCluster(result.Cluster) 96 | return nil 97 | } 98 | 99 | func init() { 100 | GetCommand.Flags().StringVarP(&getOptions.namespace, "namespace", "n", "", "The namespace of the resource") 101 | GetCommand.Flags().StringVarP(&getOptions.cluster, "cluster", "c", "", "The cluster of the resource") 102 | } 103 | -------------------------------------------------------------------------------- /cmd/client/command/helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "strings" 27 | 28 | "github.com/fatih/color" 29 | 30 | "github.com/olekukonko/tablewriter" 31 | 32 | "github.com/apache/kvrocks-controller/store" 33 | ) 34 | 35 | func printLine(format string, a ...interface{}) { 36 | boldColor := color.New(color.Bold) 37 | _, _ = fmt.Fprintln(os.Stdout, boldColor.Sprintf(format, a...)) 38 | } 39 | 40 | func printCluster(cluster *store.Cluster) { 41 | writer := tablewriter.NewWriter(os.Stdout) 42 | printLine("") 43 | printLine("cluster: %s", cluster.Name) 44 | printLine("version: %d\n", cluster.Version.Load()) 45 | writer.SetHeader([]string{"SHARD", "NODE_ID", "ADDRESS", "ROLE", "MIGRATING"}) 46 | writer.SetCenterSeparator("|") 47 | for i, shard := range cluster.Shards { 48 | for _, node := range shard.Nodes { 49 | role := strings.ToUpper(store.RoleSlave) 50 | if node.IsMaster() { 51 | role = strings.ToUpper(store.RoleMaster) 52 | } 53 | migratingStatus := "NO" 54 | if shard.IsMigrating() { 55 | migratingStatus = fmt.Sprintf("%s --> %d", shard.MigratingSlot, shard.TargetShardIndex) 56 | } 57 | columns := []string{fmt.Sprintf("%d", i), node.ID(), node.Addr(), role, migratingStatus} 58 | writer.Append(columns) 59 | } 60 | } 61 | writer.Render() 62 | } 63 | -------------------------------------------------------------------------------- /cmd/client/command/import.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "strings" 27 | 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | type ImportOptions struct { 32 | namespace string 33 | cluster string 34 | nodes []string 35 | password string 36 | } 37 | 38 | var importOptions ImportOptions 39 | 40 | var ImportCommand = &cobra.Command{ 41 | Use: "import", 42 | Short: "Import data from a cluster", 43 | Example: ` 44 | # Import a cluster from nodes 45 | kvctl import cluster --nodes 127.0.0.1:6379,127.0.0.1:6380 46 | `, 47 | PreRunE: importPreRun, 48 | RunE: func(cmd *cobra.Command, args []string) error { 49 | host, _ := cmd.Flags().GetString("host") 50 | client := newClient(host) 51 | resource := strings.ToLower(args[0]) 52 | switch resource { 53 | case ResourceCluster: 54 | importOptions.cluster = args[1] 55 | return importCluster(client, &importOptions) 56 | default: 57 | return fmt.Errorf("unsupported resource type: %s", resource) 58 | } 59 | }, 60 | SilenceUsage: true, 61 | SilenceErrors: true, 62 | } 63 | 64 | func importPreRun(_ *cobra.Command, args []string) error { 65 | if len(args) < 2 { 66 | return errors.New("missing resource name") 67 | } 68 | if importOptions.namespace == "" { 69 | return errors.New("missing namespace, please specify with -n or --namespace") 70 | } 71 | if len(importOptions.nodes) == 0 { 72 | return errors.New("missing nodes") 73 | } 74 | return nil 75 | } 76 | 77 | func importCluster(client *client, options *ImportOptions) error { 78 | rsp, err := client.restyCli.R(). 79 | SetPathParam("namespace", options.namespace). 80 | SetPathParam("cluster", options.cluster). 81 | SetBody(map[string]interface{}{ 82 | "nodes": options.nodes, 83 | "password": options.password, 84 | }). 85 | Post("/namespaces/{namespace}/clusters/{cluster}/import") 86 | if err != nil { 87 | return err 88 | } 89 | if rsp.IsError() { 90 | return errors.New(rsp.String()) 91 | } 92 | printLine("import cluster: %s successfully.", options.cluster) 93 | return nil 94 | } 95 | 96 | func init() { 97 | ImportCommand.Flags().StringVarP(&importOptions.namespace, "namespace", "n", "", "The namespace of the cluster") 98 | ImportCommand.Flags().StringVarP(&importOptions.cluster, "cluster", "c", "", "The cluster name") 99 | ImportCommand.Flags().StringSliceVarP(&importOptions.nodes, "nodes", "", nil, "The nodes to import from") 100 | ImportCommand.Flags().StringVarP(&importOptions.password, "password", "p", "", "The password of the cluster") 101 | } 102 | -------------------------------------------------------------------------------- /cmd/client/command/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var listOptions struct { 31 | namespace string 32 | cluster string 33 | } 34 | 35 | var ListCommand = &cobra.Command{ 36 | Use: "list", 37 | Short: "Display all resources", 38 | Example: ` 39 | # Display all namespaces 40 | kvctl list namespaces 41 | 42 | # Display all clusters in the namespace 43 | kvctl list clusters -n 44 | 45 | # Display all nodes in the cluster 46 | kvctl list nodes -n -c 47 | `, 48 | ValidArgs: []string{"namespaces", "clusters", "shards", "nodes"}, 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | host, _ := cmd.Flags().GetString("host") 51 | client := newClient(host) 52 | switch strings.ToLower(args[0]) { 53 | case "namespaces": 54 | return listNamespace(client) 55 | case "clusters": 56 | return listClusters(client) 57 | default: 58 | return fmt.Errorf("unsupported resource type %s", args[0]) 59 | } 60 | }, 61 | PreRunE: listPreRun, 62 | SilenceUsage: true, 63 | SilenceErrors: true, 64 | } 65 | 66 | func listPreRun(_ *cobra.Command, args []string) error { 67 | if len(args) == 0 { 68 | return fmt.Errorf("missing resource type, please specify one of [namespaces, clusters, nodes]") 69 | } 70 | 71 | resource := strings.ToLower(args[0]) 72 | if resource == "namespaces" { 73 | return nil 74 | } 75 | if listOptions.namespace == "" { 76 | return fmt.Errorf("missing namespace, please specify the namespace via -n or --namespace option") 77 | } 78 | if resource == "nodes" && listOptions.cluster == "" { 79 | return fmt.Errorf("missing cluster, please specify the cluster via -c or --cluster option") 80 | } 81 | return nil 82 | } 83 | 84 | func listNamespace(cli *client) error { 85 | rsp, err := cli.restyCli.R().Get("/namespaces") 86 | if err != nil { 87 | return err 88 | } 89 | if rsp.IsError() { 90 | return unmarshalError(rsp.Body()) 91 | } 92 | 93 | var result struct { 94 | Namespaces []string `json:"namespaces"` 95 | } 96 | if err := unmarshalData(rsp.Body(), &result); err != nil { 97 | return err 98 | } 99 | if len(result.Namespaces) == 0 { 100 | printLine("no namespace found.") 101 | return nil 102 | } 103 | for _, ns := range result.Namespaces { 104 | printLine(ns) 105 | } 106 | return nil 107 | } 108 | 109 | func listClusters(cli *client) error { 110 | rsp, err := cli.restyCli.R(). 111 | SetPathParam("namespace", listOptions.namespace). 112 | Get("/namespaces/{namespace}/clusters") 113 | if err != nil { 114 | return err 115 | } 116 | if rsp.IsError() { 117 | return unmarshalError(rsp.Body()) 118 | } 119 | 120 | var result struct { 121 | Clusters []string `json:"clusters"` 122 | } 123 | if err := unmarshalData(rsp.Body(), &result); err != nil { 124 | return err 125 | } 126 | if len(result.Clusters) == 0 { 127 | printLine("no cluster found.") 128 | return nil 129 | } 130 | for _, cluster := range result.Clusters { 131 | printLine(cluster) 132 | } 133 | return nil 134 | } 135 | 136 | func init() { 137 | ListCommand.Flags().StringVarP(&listOptions.namespace, "namespace", "n", "", "The namespace") 138 | ListCommand.Flags().StringVarP(&listOptions.cluster, "cluster", "c", "", "The cluster") 139 | } 140 | -------------------------------------------------------------------------------- /cmd/client/command/migrate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package command 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "strconv" 27 | "strings" 28 | 29 | "github.com/apache/kvrocks-controller/store" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | type MigrationOptions struct { 34 | namespace string 35 | cluster string 36 | slot string 37 | target int 38 | slotOnly bool 39 | } 40 | 41 | var migrateOptions MigrationOptions 42 | 43 | var MigrateCommand = &cobra.Command{ 44 | Use: "migrate", 45 | Short: "Migrate slot to another node", 46 | Example: ` 47 | # Migrate slot between cluster shards 48 | kvctl migrate slot --target -n -c 49 | `, 50 | PreRunE: migrationPreRun, 51 | RunE: func(cmd *cobra.Command, args []string) error { 52 | host, _ := cmd.Flags().GetString("host") 53 | client := newClient(host) 54 | resource := strings.ToLower(args[0]) 55 | switch resource { 56 | case "slot": 57 | return migrateSlot(client, &migrateOptions) 58 | default: 59 | return fmt.Errorf("unsupported resource type: %s", resource) 60 | } 61 | }, 62 | SilenceUsage: true, 63 | SilenceErrors: true, 64 | } 65 | 66 | func migrationPreRun(_ *cobra.Command, args []string) error { 67 | if len(args) < 1 { 68 | return fmt.Errorf("resource type should be specified") 69 | } 70 | if len(args) < 2 { 71 | return fmt.Errorf("the slot number should be specified") 72 | } 73 | _, err := store.ParseSlotRange(args[1]) 74 | if err != nil { 75 | return fmt.Errorf("invalid slot number: %s, error: %w", args[1], err) 76 | } 77 | migrateOptions.slot = args[1] 78 | 79 | if migrateOptions.namespace == "" { 80 | return fmt.Errorf("namespace is required, please specify with -n or --namespace") 81 | } 82 | if migrateOptions.cluster == "" { 83 | return fmt.Errorf("cluster is required, please specify with -c or --cluster") 84 | } 85 | if migrateOptions.target < 0 { 86 | return fmt.Errorf("target is required, please specify with --target") 87 | } 88 | return nil 89 | } 90 | 91 | func migrateSlot(client *client, options *MigrationOptions) error { 92 | rsp, err := client.restyCli.R(). 93 | SetPathParam("namespace", options.namespace). 94 | SetPathParam("cluster", options.cluster). 95 | SetBody(map[string]interface{}{ 96 | "slot": options.slot, 97 | "target": options.target, 98 | "slotOnly": strconv.FormatBool(options.slotOnly), 99 | }). 100 | Post("/namespaces/{namespace}/clusters/{cluster}/migrate") 101 | if err != nil { 102 | return err 103 | } 104 | if rsp.IsError() { 105 | return errors.New(rsp.String()) 106 | } 107 | printLine("migrate slot[%s] task is submitted successfully.", options.slot) 108 | return nil 109 | } 110 | 111 | func init() { 112 | MigrateCommand.Flags().StringVar(&migrateOptions.slot, "slot", "", "The slot to migrate") 113 | MigrateCommand.Flags().IntVar(&migrateOptions.target, "target", -1, "The target node") 114 | MigrateCommand.Flags().StringVarP(&migrateOptions.namespace, "namespace", "n", "", "The namespace") 115 | MigrateCommand.Flags().StringVarP(&migrateOptions.cluster, "cluster", "c", "", "The cluster") 116 | MigrateCommand.Flags().BoolVar(&migrateOptions.slotOnly, "slot-only", false, "Only migrate slot and ignore the existing data") 117 | } 118 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package main 22 | 23 | import ( 24 | "os" 25 | 26 | "github.com/fatih/color" 27 | 28 | "github.com/spf13/cobra" 29 | 30 | "github.com/apache/kvrocks-controller/cmd/client/command" 31 | ) 32 | 33 | var rootCommand = &cobra.Command{ 34 | Use: "kvctl", 35 | Short: "kvctl is a command line tool for the Kvrocks controller service", 36 | Run: func(cmd *cobra.Command, args []string) { 37 | _, _ = color.New(color.Bold).Println("Run 'kvctl --help' for usage.") 38 | os.Exit(0) 39 | }, 40 | } 41 | 42 | func main() { 43 | if err := rootCommand.Execute(); err != nil { 44 | color.Red("error: %v", err) 45 | os.Exit(1) 46 | } 47 | } 48 | 49 | func init() { 50 | rootCommand.PersistentFlags().StringP("host", "H", 51 | "http://127.0.0.1:9379", "The host of the Kvrocks controller service") 52 | 53 | rootCommand.AddCommand(command.ListCommand) 54 | rootCommand.AddCommand(command.CreateCommand) 55 | rootCommand.AddCommand(command.GetCommand) 56 | rootCommand.AddCommand(command.DeleteCommand) 57 | rootCommand.AddCommand(command.ImportCommand) 58 | rootCommand.AddCommand(command.MigrateCommand) 59 | rootCommand.AddCommand(command.FailoverCommand) 60 | rootCommand.AddCommand(command.RaftCommand) 61 | 62 | rootCommand.SilenceUsage = true 63 | rootCommand.SilenceErrors = true 64 | } 65 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package main 21 | 22 | import ( 23 | "context" 24 | "flag" 25 | "os" 26 | "os/signal" 27 | "syscall" 28 | 29 | "github.com/apache/kvrocks-controller/config" 30 | "github.com/apache/kvrocks-controller/logger" 31 | "github.com/apache/kvrocks-controller/server" 32 | "github.com/apache/kvrocks-controller/version" 33 | 34 | "go.uber.org/zap" 35 | "gopkg.in/yaml.v1" 36 | ) 37 | 38 | var configPath string 39 | 40 | func init() { 41 | flag.StringVar(&configPath, "c", "config/config.yaml", "set config yaml file path") 42 | } 43 | 44 | func registerSignal(closeFn func()) { 45 | c := make(chan os.Signal, 1) 46 | signal.Notify(c, []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1}...) 47 | go func() { 48 | for sig := range c { 49 | if handleSignals(sig) { 50 | closeFn() 51 | return 52 | } 53 | } 54 | }() 55 | } 56 | 57 | func handleSignals(sig os.Signal) (exitNow bool) { 58 | switch sig { 59 | case syscall.SIGINT, syscall.SIGTERM: 60 | logger.Get().With(zap.String("signal", sig.String())).Info("Got signal to exit") 61 | return true 62 | default: 63 | return false 64 | } 65 | } 66 | 67 | func main() { 68 | defer logger.Sync() 69 | 70 | ctx, cancelFn := context.WithCancel(context.Background()) 71 | // os signal handler 72 | shutdownCh := make(chan struct{}) 73 | registerSignal(func() { 74 | close(shutdownCh) 75 | cancelFn() 76 | }) 77 | 78 | flag.Parse() 79 | 80 | logger.Get().Info("Kvrocks controller is running with version: " + version.Version) 81 | cfg := config.Default() 82 | if len(configPath) != 0 { 83 | content, err := os.ReadFile(configPath) 84 | if err != nil { 85 | logger.Get().With(zap.Error(err)).Error("Failed to read the config file") 86 | return 87 | } 88 | if err := yaml.Unmarshal(content, cfg); err != nil { 89 | logger.Get().With(zap.Error(err)).Error("Failed to unmarshal the config file") 90 | return 91 | } 92 | } 93 | if err := cfg.Validate(); err != nil { 94 | logger.Get().With(zap.Error(err)).Error("Failed to validate the config file") 95 | return 96 | } 97 | 98 | if cfg.Log != nil && cfg.Log.Filename != "" { 99 | logger.Get().Info("Logs will be saved to " + cfg.Log.Filename) 100 | if err := logger.InitLoggerRotate(cfg.Log.Level, cfg.Log.Filename, cfg.Log.MaxBackups, cfg.Log.MaxAge, cfg.Log.MaxSize, cfg.Log.Compress); err != nil { 101 | logger.Get().With(zap.Error(err)).Error("Failed to init the log rotate") 102 | return 103 | } 104 | } 105 | 106 | srv, err := server.NewServer(cfg) 107 | if err != nil { 108 | logger.Get().With(zap.Error(err)).Error("Failed to create the server") 109 | return 110 | } 111 | if err := srv.Start(ctx); err != nil { 112 | logger.Get().With(zap.Error(err)).Error("Failed to start the server") 113 | return 114 | } 115 | 116 | // wait for the term signal 117 | <-shutdownCh 118 | if err := srv.Stop(); err != nil { 119 | logger.Get().With(zap.Error(err)).Error("Failed to close the server") 120 | } else { 121 | logger.Get().Info("Bye bye, Kvrocks controller was exited") 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /config/config-consul.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | addr: "127.0.0.1:9379" 20 | 21 | # Which store engine should be used by controller 22 | # options: etcd, zookeeper, raft, consul 23 | # Note: the raft engine is an experimental feature and is not recommended for production use. 24 | # 25 | # default: etcd 26 | storage_type: consul 27 | 28 | consul: 29 | addrs: 30 | - "127.0.0.1:8500" 31 | elect_path: 32 | tls: 33 | enable: false 34 | cert_file: 35 | key_file: 36 | ca_file: 37 | 38 | controller: 39 | failover: 40 | ping_interval_seconds: 3 41 | min_alive_size: 5 42 | # Uncomment this part to save logs to filename instead of stdout 43 | #log: 44 | # level: info 45 | # filename: /data/logs/kvctl.log 46 | # max_backups: 10 47 | # max_age: 7 48 | # max_size: 100 49 | # compress: false 50 | -------------------------------------------------------------------------------- /config/config-raft.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | addr: "127.0.0.1:9379" 20 | 21 | 22 | # Which store engine should be used by controller 23 | # options: etcd, zookeeper, raft, consul 24 | # Note: the raft engine is an experimental feature and is not recommended for production use. 25 | # 26 | # default: etcd 27 | storage_type: raft 28 | 29 | raft: 30 | id: 1 31 | data_dir: "/data/kvrocks/raft" 32 | # Cluster state must be 'new' or 'existing'. 33 | # For a new cluster, its initial cluster state must be set to 'new'. 34 | # And you would like to join an existing cluster, its initial cluster state must be set to 'existing'. 35 | cluster_state: "new" 36 | peers: 37 | - "http://127.0.0.1:6001" 38 | - "http://127.0.0.1:6002" 39 | - "http://127.0.0.1:6003" 40 | 41 | controller: 42 | failover: 43 | ping_interval_seconds: 3 44 | min_alive_size: 5 45 | 46 | # Uncomment this part to save logs to filename instead of stdout 47 | #log: 48 | # level: info 49 | # filename: /data/logs/kvctl.log 50 | # max_backups: 10 51 | # max_age: 7 52 | # max_size: 100 53 | # compress: false -------------------------------------------------------------------------------- /config/config-zk.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | addr: "127.0.0.1:9379" 20 | 21 | 22 | # Which store engine should be used by controller 23 | # options: etcd, zookeeper, raft, consul 24 | # Note: the raft engine is an experimental feature and is not recommended for production use. 25 | # 26 | # default: etcd 27 | storage_type: zookeeper 28 | 29 | zookeeper: 30 | addrs: 31 | - "127.0.0.1:2181" 32 | scheme: 33 | auth: 34 | elect_path: 35 | 36 | controller: 37 | failover: 38 | ping_interval_seconds: 3 39 | min_alive_size: 5 40 | 41 | # Uncomment this part to save logs to filename instead of stdout 42 | #log: 43 | # level: info 44 | # filename: /data/logs/kvctl.log 45 | # max_backups: 10 46 | # max_age: 7 47 | # max_size: 100 48 | # compress: false -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | addr: "127.0.0.1:9379" 20 | 21 | 22 | # Which store engine should be used by controller 23 | # options: etcd, zookeeper, raft, consul 24 | # Note: the raft engine is an experimental feature and is not recommended for production use. 25 | # 26 | # default: etcd 27 | storage_type: etcd 28 | 29 | etcd: 30 | addrs: 31 | - "127.0.0.1:2379" 32 | username: 33 | password: 34 | elect_path: 35 | tls: 36 | enable: false 37 | cert_file: 38 | key_file: 39 | ca_file: 40 | 41 | controller: 42 | failover: 43 | ping_interval_seconds: 3 44 | min_alive_size: 5 45 | 46 | # Uncomment this part to save logs to filename instead of stdout 47 | #log: 48 | # level: info 49 | # filename: /data/logs/kvctl.log 50 | # max_backups: 10 51 | # max_age: 7 52 | # max_size: 100 53 | # compress: false -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package config 21 | 22 | import ( 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestDefaultControllerConfigSet(t *testing.T) { 29 | cfg := Default() 30 | expectedControllerConfig := &ControllerConfig{ 31 | FailOver: &FailOverConfig{ 32 | PingIntervalSeconds: 3, 33 | MaxPingCount: 5, 34 | }, 35 | } 36 | 37 | assert.Equal(t, expectedControllerConfig, cfg.Controller) 38 | } 39 | -------------------------------------------------------------------------------- /consts/context_key.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package consts 21 | 22 | const ( 23 | ContextKeyStore = "_context_key_storage" 24 | ContextKeyCluster = "_context_key_cluster" 25 | ContextKeyClusterShard = "_context_key_cluster_shard" 26 | ContextKeyRaftNode = "_context_key_raft_node" 27 | ) 28 | 29 | const ( 30 | HeaderIsRedirect = "X-Is-Redirect" 31 | HeaderDontCheckClusterMode = "X-Dont-Check-Cluster-Mode" 32 | ) 33 | -------------------------------------------------------------------------------- /consts/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package consts 22 | 23 | import "errors" 24 | 25 | var ( 26 | ErrInvalidArgument = errors.New("invalid argument") 27 | ErrNotFound = errors.New("not found") 28 | ErrForbidden = errors.New("forbidden") 29 | ErrAlreadyExists = errors.New("already exists") 30 | ErrIndexOutOfRange = errors.New("index out of range") 31 | ErrShardIsSame = errors.New("source and target shard is same") 32 | ErrSlotOutOfRange = errors.New("slot out of range") 33 | ErrSlotNotBelongToAnyShard = errors.New("slot not belong to any shard") 34 | ErrSlotRangeBelongsToMultipleShards = errors.New("slot range belongs to multiple shards") 35 | ErrNodeIsNotMaster = errors.New("the old node is not master") 36 | ErrOldMasterNodeNotFound = errors.New("old master node not found") 37 | ErrShardNoReplica = errors.New("no replica in shard") 38 | ErrShardIsServicing = errors.New("shard is servicing") 39 | ErrShardSlotIsMigrating = errors.New("shard slot is migrating") 40 | ErrShardNoMatchNewMaster = errors.New("no match new master in shard") 41 | ErrSlotStartAndStopEqual = errors.New("start and stop of a range cannot be equal") 42 | ) 43 | -------------------------------------------------------------------------------- /controller/controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package controller 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/require" 28 | 29 | "github.com/apache/kvrocks-controller/config" 30 | "github.com/apache/kvrocks-controller/consts" 31 | "github.com/apache/kvrocks-controller/store" 32 | "github.com/apache/kvrocks-controller/store/engine" 33 | ) 34 | 35 | func TestController_Basics(t *testing.T) { 36 | ctx := context.Background() 37 | ns := "test-ns" 38 | cluster0, err := store.NewCluster("test-cluster-0", []string{"127.0.0.1:7770"}, 1) 39 | require.NoError(t, err) 40 | cluster1, err := store.NewCluster("test-cluster-1", []string{"127.0.0.1:7771"}, 1) 41 | require.NoError(t, err) 42 | 43 | s := store.NewClusterStore(engine.NewMock()) 44 | require.True(t, s.IsLeader()) 45 | require.NoError(t, s.CreateCluster(ctx, ns, cluster0)) 46 | require.NoError(t, s.CreateCluster(ctx, ns, cluster1)) 47 | 48 | c, err := New(s, &config.ControllerConfig{ 49 | FailOver: &config.FailOverConfig{ 50 | PingIntervalSeconds: 1, 51 | }, 52 | }) 53 | require.NoError(t, err) 54 | require.NoError(t, c.Start(ctx)) 55 | defer func() { 56 | c.Close() 57 | }() 58 | 59 | c.WaitForReady() 60 | 61 | t.Run("get cluster", func(t *testing.T) { 62 | cluster, err := c.getCluster(ns, "test-cluster-0") 63 | require.NoError(t, err) 64 | require.NotNil(t, cluster) 65 | cluster, err = c.getCluster(ns, "test-cluster-1") 66 | require.NoError(t, err) 67 | require.NotNil(t, cluster) 68 | _, err = c.getCluster(ns, "test-cluster-2") 69 | require.ErrorIs(t, err, consts.ErrNotFound) 70 | require.NotNil(t, cluster) 71 | }) 72 | 73 | t.Run("remove cluster", func(t *testing.T) { 74 | c.removeCluster(ns, "test-cluster-1") 75 | _, err = c.getCluster(ns, "test-cluster-1") 76 | require.ErrorIs(t, err, consts.ErrNotFound) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /docs/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kvrocks-controller/cfc57e27bfb5b5fc7383bad9ae478786411dede0/docs/images/overview.png -------------------------------------------------------------------------------- /docs/images/server.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kvrocks-controller/cfc57e27bfb5b5fc7383bad9ae478786411dede0/docs/images/server.gif -------------------------------------------------------------------------------- /licenses/LICENSE-atomic.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/LICENSE-gin.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel Martínez-Almeida 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/LICENSE-go-redis.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 The github.com/redis/go-redis Authors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /licenses/LICENSE-resty.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 Jeevanandam M., https://myjeeva.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /licenses/LICENSE-tablewriter.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 by Oleku Konko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/LICENSE-testify.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /licenses/LICENSE-validator.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dean Karn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /licenses/LICENSE-zap.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/LICENSE-zk.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Samuel Stauffer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the author nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package logger 21 | 22 | import ( 23 | "fmt" 24 | 25 | "go.uber.org/zap" 26 | "go.uber.org/zap/zapcore" 27 | "gopkg.in/natefinch/lumberjack.v2" 28 | ) 29 | 30 | var zapLogger *zap.Logger 31 | 32 | func Get() *zap.Logger { 33 | return zapLogger 34 | } 35 | 36 | func init() { 37 | zapConfig := zap.NewProductionConfig() 38 | zapConfig.EncoderConfig.TimeKey = "timestamp" 39 | zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 40 | zapLogger, _ = zapConfig.Build() 41 | } 42 | 43 | func getEncoder() zapcore.Encoder { 44 | encoderConfig := zap.NewProductionEncoderConfig() 45 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 46 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 47 | encoderConfig.TimeKey = "time" 48 | 49 | return zapcore.NewJSONEncoder(encoderConfig) 50 | } 51 | 52 | func getWriteSyncer(filename string, maxBackups, maxAge, maxSize int, compress bool) (zapcore.WriteSyncer, error) { 53 | lumberJackLogger := &lumberjack.Logger{ 54 | Filename: filename, 55 | MaxBackups: maxBackups, 56 | MaxSize: maxSize, 57 | MaxAge: maxAge, 58 | Compress: compress, 59 | } 60 | 61 | if _, err := lumberJackLogger.Write([]byte("test logfile writable\r\n")); err != nil { 62 | return nil, fmt.Errorf("test writing to log file %s failed", filename) 63 | } 64 | 65 | return zapcore.AddSync(lumberJackLogger), nil 66 | } 67 | 68 | func InitLoggerRotate(level, filename string, maxBackups, maxAge, maxSize int, compress bool) error { 69 | // if file path is empty, use default zapLogger, print in console 70 | if len(filename) == 0 { 71 | return nil 72 | } 73 | if level != "info" && level != "warn" && level != "error" { 74 | return fmt.Errorf("log level must be one of info,warn,error") 75 | } 76 | if maxBackups > 100 || maxBackups < 10 { 77 | return fmt.Errorf("log max_backups must be between 10 and 100") 78 | } 79 | if maxAge > 30 || maxAge < 1 { 80 | return fmt.Errorf("log max_age must be between 1 and 30") 81 | } 82 | if maxSize > 500 || maxSize < 100 { 83 | return fmt.Errorf("log max_size must be between 100 and 500") 84 | } 85 | 86 | var l = new(zapcore.Level) 87 | if err := l.UnmarshalText([]byte(level)); err != nil { 88 | return err 89 | } 90 | encoder := getEncoder() 91 | writeSync, err := getWriteSyncer(filename, maxBackups, maxAge, maxSize, compress) 92 | if err != nil { 93 | return err 94 | } 95 | core := zapcore.NewCore(encoder, writeSync, l) 96 | rotateLogger := zap.New(core, zap.AddCaller()) 97 | zapLogger = rotateLogger 98 | 99 | return nil 100 | } 101 | 102 | func Sync() { 103 | if zapLogger != nil { 104 | if err := zapLogger.Sync(); err != nil { 105 | return 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /metrics/setup.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package metrics 21 | 22 | import ( 23 | "strings" 24 | 25 | "github.com/prometheus/client_golang/prometheus" 26 | ) 27 | 28 | type performanceMetrics struct { 29 | Latencies *prometheus.HistogramVec 30 | HTTPCodes *prometheus.CounterVec 31 | Payload *prometheus.CounterVec 32 | HTTPServerPanics *prometheus.CounterVec 33 | } 34 | 35 | var _metrics *performanceMetrics 36 | 37 | const ( 38 | _namespace = "kvrocks" 39 | _subsystem = "controller" 40 | ) 41 | 42 | // NewHistogramHelper was used to fast create and register prometheus histogram metric 43 | func NewHistogramHelper(ns, subsystem, name string, buckets []float64, labels ...string) *prometheus.HistogramVec { 44 | ns = strings.ReplaceAll(ns, "-", "_") 45 | subsystem = strings.ReplaceAll(subsystem, "-", "_") 46 | name = strings.ReplaceAll(name, "-", "_") 47 | opts := prometheus.HistogramOpts{} 48 | opts.Namespace = ns 49 | opts.Subsystem = subsystem 50 | opts.Name = name 51 | opts.Help = name 52 | opts.Buckets = buckets 53 | histogram := prometheus.NewHistogramVec(opts, labels) 54 | prometheus.MustRegister(histogram) 55 | return histogram 56 | } 57 | 58 | // NewCounterHelper was used to fast create and register prometheus counter metric 59 | func NewCounterHelper(ns, subsystem, name string, labels ...string) *prometheus.CounterVec { 60 | ns = strings.ReplaceAll(ns, "-", "_") 61 | subsystem = strings.ReplaceAll(subsystem, "-", "_") 62 | opts := prometheus.CounterOpts{} 63 | opts.Namespace = ns 64 | opts.Subsystem = subsystem 65 | opts.Name = name 66 | opts.Help = name 67 | counters := prometheus.NewCounterVec(opts, labels) 68 | prometheus.MustRegister(counters) 69 | return counters 70 | } 71 | 72 | func setupMetrics() { 73 | labels := []string{"host", "uri", "method", "code"} 74 | buckets := prometheus.ExponentialBuckets(1, 2, 16) 75 | newHistogram := func(name string, labels ...string) *prometheus.HistogramVec { 76 | return NewHistogramHelper(_namespace, _subsystem, name, buckets, labels...) 77 | } 78 | newCounter := func(name string, labels ...string) *prometheus.CounterVec { 79 | return NewCounterHelper(_namespace, _subsystem, name, labels...) 80 | } 81 | _metrics = &performanceMetrics{ 82 | Latencies: newHistogram("request_latency", labels...), 83 | HTTPCodes: newCounter("http_code", labels...), 84 | Payload: newCounter("http_payload", labels...), 85 | } 86 | } 87 | 88 | func Get() *performanceMetrics { 89 | return _metrics 90 | } 91 | 92 | func init() { 93 | setupMetrics() 94 | } 95 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | set -e 20 | 21 | GO_PROJECT=github.com/apache/kvrocks-controller 22 | BUILD_DIR=./_build 23 | VERSION=`cat VERSION.txt` 24 | 25 | SERVER_TARGET_NAME=kvctl-server 26 | CLIENT_TARGET_NAME=kvctl 27 | 28 | for TARGET_NAME in "$SERVER_TARGET_NAME" "$CLIENT_TARGET_NAME"; do 29 | if [[ "$TARGET_NAME" == "$SERVER_TARGET_NAME" ]]; then 30 | CMD_PATH="${GO_PROJECT}/cmd/server" 31 | else 32 | CMD_PATH="${GO_PROJECT}/cmd/client" 33 | fi 34 | 35 | CGO_ENABLED=0 go build -v -ldflags \ 36 | "-X $GO_PROJECT/version.Version=$VERSION" \ 37 | -o ${TARGET_NAME} ${CMD_PATH} 38 | 39 | if [[ $? -ne 0 ]]; then 40 | echo "Failed to build $TARGET_NAME" 41 | exit 1 42 | fi 43 | 44 | echo "Build $TARGET_NAME successfully" 45 | done 46 | 47 | rm -rf ${BUILD_DIR} 48 | mkdir -p ${BUILD_DIR} 49 | mv $SERVER_TARGET_NAME ${BUILD_DIR} 50 | mv $CLIENT_TARGET_NAME ${BUILD_DIR} 51 | -------------------------------------------------------------------------------- /scripts/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | version: "3.7" 20 | 21 | services: 22 | kvrocks0: 23 | build: ./kvrocks 24 | container_name: kvrocks-cluster 25 | ports: 26 | - "7770:7770" 27 | - "7771:7771" 28 | 29 | 30 | etcd0: 31 | image: "quay.io/coreos/etcd:v3.5.17" 32 | container_name: etcd0 33 | ports: 34 | - "2380:2380" 35 | - "2379:2379" 36 | environment: 37 | - ALLOW_NONE_AUTHENTICATION=yes 38 | - ETCD_NAME=etcd0 39 | - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380 40 | - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 41 | - ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2379 42 | - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd0:2380 43 | - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster 44 | - ETCD_INITIAL_CLUSTER=etcd0=http://etcd0:2380 45 | - ETCD_INITIAL_CLUSTER_STATE=new 46 | 47 | zookeeper0: 48 | image: "zookeeper:latest" 49 | container_name: zookeeper0 50 | ports: 51 | - "2181:2181" 52 | 53 | consul0: 54 | image: hashicorp/consul:latest 55 | container_name: consul0 56 | ports: 57 | - "8500:8500" 58 | command: "agent -dev -client=0.0.0.0" 59 | 60 | postgres0: 61 | build: ./pg-dockerfile 62 | container_name: postgres0 63 | environment: 64 | POSTGRES_USER: postgres 65 | POSTGRES_PASSWORD: postgres 66 | POSTGRES_DB: testdb 67 | ports: 68 | - "5432:5432" 69 | volumes: 70 | - ./pg-init-scripts:/docker-entrypoint-initdb.d 71 | -------------------------------------------------------------------------------- /scripts/docker/kvrocks/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM apache/kvrocks:latest 2 | USER root 3 | RUN mkdir /tmp/kvrocks7770 /tmp/kvrocks7771 4 | 5 | RUN echo "kvrocks -c /var/lib/kvrocks/kvrocks.conf --port 7770 --dir /tmp/kvrocks7770 --daemonize yes --cluster-enabled yes --bind 0.0.0.0" >> start.sh 6 | RUN echo "kvrocks -c /var/lib/kvrocks/kvrocks.conf --port 7771 --dir /tmp/kvrocks7771 --cluster-enabled yes --bind 0.0.0.0" >> start.sh 7 | RUN chmod +x start.sh 8 | 9 | EXPOSE 7770:7770 10 | EXPOSE 7771:7771 11 | 12 | ENTRYPOINT ["sh","start.sh"] 13 | -------------------------------------------------------------------------------- /scripts/docker/pg-dockerfile/Dockerfile: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | FROM postgres:16 19 | 20 | RUN apt-get update && apt-get install -y \ 21 | postgresql-server-dev-16 \ 22 | build-essential \ 23 | libpq-dev \ 24 | wget 25 | 26 | RUN wget https://github.com/citusdata/pg_cron/archive/refs/tags/v1.6.5.tar.gz \ 27 | && tar -xvzf v1.6.5.tar.gz \ 28 | && cd pg_cron-1.6.5 \ 29 | && make && make install \ 30 | && cd .. && rm -rf v1.6.5.tar.gz pg_cron-1.6.5 31 | 32 | RUN echo "shared_preload_libraries = 'pg_cron'" >> /usr/share/postgresql/postgresql.conf.sample \ 33 | && echo "cron.database_name = 'testdb'" >> /usr/share/postgresql/postgresql.conf.sample -------------------------------------------------------------------------------- /scripts/docker/pg-init-scripts/init.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | CREATE TABLE locks ( 22 | name TEXT PRIMARY KEY, 23 | leaderID TEXT NOT NULL 24 | ); 25 | 26 | CREATE TABLE kv ( 27 | key TEXT PRIMARY KEY, 28 | value BYTEA 29 | ); 30 | 31 | CREATE OR REPLACE FUNCTION notify_changes() 32 | RETURNS TRIGGER AS $$ 33 | BEGIN 34 | IF TG_OP = 'INSERT' THEN 35 | PERFORM cron.schedule('delete_' || NEW.name, '6 seconds', FORMAT('DELETE FROM locks WHERE name = %L', NEW.name)); 36 | PERFORM pg_notify('lock_change', 'INSERT:' || NEW.leaderID::text); 37 | END IF; 38 | 39 | IF TG_OP = 'DELETE' THEN 40 | PERFORM cron.unschedule('delete_' || OLD.name); 41 | PERFORM pg_notify('lock_change', 'DELETE:' || OLD.leaderID::text); 42 | END IF; 43 | 44 | RETURN NULL; 45 | END; 46 | $$ LANGUAGE plpgsql; 47 | 48 | CREATE TRIGGER lock_change_trigger 49 | AFTER INSERT OR DELETE ON locks 50 | FOR EACH ROW EXECUTE FUNCTION notify_changes(); 51 | 52 | CREATE EXTENSION IF NOT EXISTS pg_cron; -------------------------------------------------------------------------------- /scripts/run-test.sh: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | set -e -x 20 | 21 | go test -v ./... -covermode=atomic -coverprofile=coverage.out -race -p 1 -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | cd docker && docker compose -p kvrocks-controller up -d --force-recreate && cd .. 20 | -------------------------------------------------------------------------------- /scripts/teardown.sh: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | set -e 20 | 21 | cd docker && docker compose -p kvrocks-controller down && cd .. 22 | -------------------------------------------------------------------------------- /server/api/handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package api 22 | 23 | import ( 24 | "github.com/apache/kvrocks-controller/store" 25 | ) 26 | 27 | type Handler struct { 28 | Namespace *NamespaceHandler 29 | Cluster *ClusterHandler 30 | Shard *ShardHandler 31 | Node *NodeHandler 32 | Raft *RaftHandler 33 | } 34 | 35 | func NewHandler(s *store.ClusterStore) *Handler { 36 | return &Handler{ 37 | Namespace: &NamespaceHandler{s: s}, 38 | Cluster: &ClusterHandler{s: s}, 39 | Shard: &ShardHandler{s: s}, 40 | Node: &NodeHandler{s: s}, 41 | Raft: &RaftHandler{}, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/api/namespace.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package api 22 | 23 | import ( 24 | "errors" 25 | 26 | "github.com/apache/kvrocks-controller/consts" 27 | 28 | "github.com/apache/kvrocks-controller/server/helper" 29 | 30 | "github.com/gin-gonic/gin" 31 | 32 | "github.com/apache/kvrocks-controller/store" 33 | ) 34 | 35 | type NamespaceHandler struct { 36 | s store.Store 37 | } 38 | 39 | func (handler *NamespaceHandler) List(c *gin.Context) { 40 | namespaces, err := handler.s.ListNamespace(c) 41 | if err != nil { 42 | helper.ResponseError(c, err) 43 | return 44 | } 45 | helper.ResponseOK(c, gin.H{"namespaces": namespaces}) 46 | } 47 | 48 | func (handler *NamespaceHandler) Exists(c *gin.Context) { 49 | namespace := c.Param("namespace") 50 | ok, err := handler.s.ExistsNamespace(c, namespace) 51 | if err != nil { 52 | helper.ResponseError(c, err) 53 | return 54 | } 55 | if !ok { 56 | helper.ResponseError(c, consts.ErrNotFound) 57 | return 58 | } 59 | helper.ResponseOK(c, nil) 60 | } 61 | 62 | func (handler *NamespaceHandler) Create(c *gin.Context) { 63 | var request struct { 64 | Namespace string `json:"namespace" validate:"required"` 65 | } 66 | if err := c.BindJSON(&request); err != nil { 67 | helper.ResponseBadRequest(c, err) 68 | return 69 | } 70 | 71 | if len(request.Namespace) == 0 { 72 | helper.ResponseBadRequest(c, errors.New("namespace should NOT be empty")) 73 | return 74 | } 75 | 76 | if err := handler.s.CreateNamespace(c, request.Namespace); err != nil { 77 | helper.ResponseError(c, err) 78 | return 79 | } 80 | helper.ResponseCreated(c, gin.H{"namespace": request.Namespace}) 81 | } 82 | 83 | func (handler *NamespaceHandler) Remove(c *gin.Context) { 84 | namespace := c.Param("namespace") 85 | if err := handler.s.RemoveNamespace(c, namespace); err != nil { 86 | helper.ResponseError(c, err) 87 | return 88 | } 89 | helper.ResponseNoContent(c) 90 | } 91 | -------------------------------------------------------------------------------- /server/api/namespace_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package api 21 | 22 | import ( 23 | "bytes" 24 | "encoding/json" 25 | "fmt" 26 | "io" 27 | "net/http" 28 | "net/http/httptest" 29 | "net/url" 30 | "testing" 31 | 32 | "github.com/apache/kvrocks-controller/store" 33 | "github.com/apache/kvrocks-controller/store/engine" 34 | 35 | "github.com/stretchr/testify/require" 36 | 37 | "github.com/gin-gonic/gin" 38 | ) 39 | 40 | func GetTestContext(recorder *httptest.ResponseRecorder) *gin.Context { 41 | gin.SetMode(gin.TestMode) 42 | ctx, _ := gin.CreateTestContext(recorder) 43 | ctx.Request = &http.Request{ 44 | Header: make(http.Header), 45 | URL: &url.URL{}, 46 | } 47 | return ctx 48 | } 49 | 50 | func TestNamespaceBasics(t *testing.T) { 51 | handler := &NamespaceHandler{s: store.NewClusterStore(engine.NewMock())} 52 | 53 | runCreate := func(t *testing.T, ns string, expectedStatusCode int) { 54 | recorder := httptest.NewRecorder() 55 | ctx := GetTestContext(recorder) 56 | ctx.Request.Body = io.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"namespace\":\"%s\"}", ns))) 57 | handler.Create(ctx) 58 | require.Equal(t, expectedStatusCode, recorder.Code) 59 | } 60 | 61 | runExists := func(t *testing.T, ns string, expectedStatusCode int) { 62 | recorder := httptest.NewRecorder() 63 | ctx := GetTestContext(recorder) 64 | ctx.Params = []gin.Param{{Key: "namespace", Value: ns}} 65 | handler.Exists(ctx) 66 | require.Equal(t, expectedStatusCode, recorder.Code) 67 | } 68 | 69 | runRemove := func(t *testing.T, ns string, expectedStatusCode int) { 70 | recorder := httptest.NewRecorder() 71 | ctx := GetTestContext(recorder) 72 | ctx.Params = []gin.Param{{Key: "namespace", Value: ns}} 73 | handler.Remove(ctx) 74 | require.Equal(t, expectedStatusCode, recorder.Code) 75 | } 76 | 77 | t.Run("create namespace", func(t *testing.T) { 78 | runCreate(t, "test0", http.StatusCreated) 79 | runCreate(t, "test1", http.StatusCreated) 80 | runCreate(t, "test0", http.StatusConflict) 81 | }) 82 | 83 | t.Run("exits namespace", func(t *testing.T) { 84 | runExists(t, "test0", http.StatusOK) 85 | runExists(t, "not-exists", http.StatusNotFound) 86 | }) 87 | 88 | t.Run("list namespace", func(t *testing.T) { 89 | recorder := httptest.NewRecorder() 90 | ctx := GetTestContext(recorder) 91 | handler.List(ctx) 92 | require.Equal(t, http.StatusOK, recorder.Code) 93 | 94 | var rsp struct { 95 | Data map[string][]string `json:"data"` 96 | } 97 | require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &rsp)) 98 | require.ElementsMatch(t, []string{"test0", "test1"}, rsp.Data["namespaces"]) 99 | }) 100 | 101 | t.Run("remove namespace", func(t *testing.T) { 102 | for _, ns := range []string{"test0", "test1"} { 103 | runRemove(t, ns, http.StatusNoContent) 104 | runRemove(t, ns, http.StatusNotFound) 105 | } 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /server/api/node.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package api 22 | 23 | import ( 24 | "strconv" 25 | 26 | "github.com/apache/kvrocks-controller/consts" 27 | "github.com/apache/kvrocks-controller/server/helper" 28 | "github.com/gin-gonic/gin" 29 | 30 | "github.com/apache/kvrocks-controller/store" 31 | ) 32 | 33 | type NodeHandler struct { 34 | s store.Store 35 | } 36 | 37 | func (handler *NodeHandler) List(c *gin.Context) { 38 | shard, _ := c.MustGet(consts.ContextKeyClusterShard).(*store.Shard) 39 | helper.ResponseOK(c, gin.H{"nodes": shard.Nodes}) 40 | } 41 | 42 | func (handler *NodeHandler) Create(c *gin.Context) { 43 | ns := c.Param("namespace") 44 | cluster, _ := c.MustGet(consts.ContextKeyCluster).(*store.Cluster) 45 | var req struct { 46 | Addr string `json:"addr" binding:"required"` 47 | Role string `json:"role"` 48 | Password string `json:"password"` 49 | } 50 | if err := c.ShouldBindJSON(&req); err != nil { 51 | helper.ResponseBadRequest(c, err) 52 | return 53 | } 54 | if req.Role == "" { 55 | req.Role = store.RoleSlave 56 | } 57 | shardIndex, _ := strconv.Atoi(c.Param("shard")) 58 | newNode, err := cluster.AddNode(shardIndex, req.Addr, req.Role, req.Password) 59 | if err != nil { 60 | helper.ResponseError(c, err) 61 | return 62 | } 63 | if err := handler.s.UpdateCluster(c, ns, cluster); err != nil { 64 | helper.ResponseError(c, err) 65 | return 66 | } 67 | helper.ResponseCreated(c, newNode.ID()) 68 | } 69 | 70 | func (handler *NodeHandler) Remove(c *gin.Context) { 71 | ns := c.Param("namespace") 72 | cluster, _ := c.MustGet(consts.ContextKeyCluster).(*store.Cluster) 73 | shardIndex, _ := strconv.Atoi(c.Param("shard")) 74 | err := cluster.RemoveNode(shardIndex, c.Param("id")) 75 | if err != nil { 76 | helper.ResponseError(c, err) 77 | return 78 | } 79 | if err := handler.s.UpdateCluster(c, ns, cluster); err != nil { 80 | helper.ResponseError(c, err) 81 | return 82 | } 83 | helper.ResponseNoContent(c) 84 | } 85 | -------------------------------------------------------------------------------- /server/api/raft.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package api 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "strings" 27 | 28 | "github.com/apache/kvrocks-controller/consts" 29 | "github.com/apache/kvrocks-controller/logger" 30 | "github.com/apache/kvrocks-controller/server/helper" 31 | "github.com/apache/kvrocks-controller/store/engine/raft" 32 | 33 | "github.com/gin-gonic/gin" 34 | "go.uber.org/zap" 35 | ) 36 | 37 | const ( 38 | OperationAdd = "add" 39 | OperationRemove = "remove" 40 | ) 41 | 42 | type RaftHandler struct{} 43 | 44 | type MemberRequest struct { 45 | ID uint64 `json:"id" validate:"required,gt=0"` 46 | Operation string `json:"operation" validate:"required"` 47 | Peer string `json:"peer,omitempty"` 48 | } 49 | 50 | func (r *MemberRequest) validate() error { 51 | r.Operation = strings.ToLower(r.Operation) 52 | if r.Operation != OperationAdd && r.Operation != OperationRemove { 53 | return fmt.Errorf("operation must be one of [%s]", 54 | strings.Join([]string{OperationAdd, OperationRemove}, ",")) 55 | } 56 | if r.Operation == OperationAdd && r.Peer == "" { 57 | return fmt.Errorf("peer should NOT be empty") 58 | } 59 | return nil 60 | } 61 | 62 | func (handler *RaftHandler) ListPeers(c *gin.Context) { 63 | raftNode, _ := c.MustGet(consts.ContextKeyRaftNode).(*raft.Node) 64 | helper.ResponseOK(c, gin.H{ 65 | "leader": raftNode.GetRaftLead(), 66 | "peers": raftNode.ListPeers(), 67 | }) 68 | } 69 | 70 | func (handler *RaftHandler) UpdatePeer(c *gin.Context) { 71 | var req MemberRequest 72 | if err := c.BindJSON(&req); err != nil { 73 | helper.ResponseBadRequest(c, err) 74 | return 75 | } 76 | if err := req.validate(); err != nil { 77 | helper.ResponseBadRequest(c, err) 78 | return 79 | } 80 | 81 | raftNode, _ := c.MustGet(consts.ContextKeyRaftNode).(*raft.Node) 82 | peers := raftNode.ListPeers() 83 | 84 | var err error 85 | if req.Operation == OperationAdd { 86 | for _, peer := range peers { 87 | if peer == req.Peer { 88 | helper.ResponseError(c, fmt.Errorf("peer '%s' already exists", req.Peer)) 89 | return 90 | } 91 | } 92 | err = raftNode.AddPeer(c, req.ID, req.Peer) 93 | } else { 94 | if _, ok := peers[req.ID]; !ok { 95 | helper.ResponseBadRequest(c, errors.New("peer not exists")) 96 | return 97 | } 98 | if len(peers) == 1 { 99 | helper.ResponseBadRequest(c, errors.New("can't remove the last peer")) 100 | return 101 | } 102 | err = raftNode.RemovePeer(c, req.ID) 103 | } 104 | if err != nil { 105 | helper.ResponseError(c, err) 106 | } else { 107 | logger.Get().With(zap.Any("request", req)).Info("Update peer success") 108 | helper.ResponseOK(c, nil) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /server/helper/helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package helper 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "net/http" 27 | "strings" 28 | 29 | "github.com/gin-gonic/gin" 30 | 31 | "github.com/apache/kvrocks-controller/consts" 32 | "github.com/apache/kvrocks-controller/util" 33 | ) 34 | 35 | type Error struct { 36 | Message string `json:"message"` 37 | } 38 | 39 | type Response struct { 40 | Error *Error `json:"error,omitempty"` 41 | Data interface{} `json:"data"` 42 | } 43 | 44 | func ResponseOK(c *gin.Context, data interface{}) { 45 | responseData(c, http.StatusOK, data) 46 | } 47 | 48 | func ResponseCreated(c *gin.Context, data interface{}) { 49 | responseData(c, http.StatusCreated, data) 50 | } 51 | 52 | func ResponseNoContent(c *gin.Context) { 53 | c.JSON(http.StatusNoContent, nil) 54 | } 55 | 56 | func ResponseBadRequest(c *gin.Context, err error) { 57 | c.JSON(http.StatusBadRequest, Response{ 58 | Error: &Error{Message: err.Error()}, 59 | }) 60 | } 61 | 62 | func responseData(c *gin.Context, code int, data interface{}) { 63 | c.JSON(code, Response{ 64 | Data: data, 65 | }) 66 | } 67 | 68 | func ResponseError(c *gin.Context, err error) { 69 | code := http.StatusInternalServerError 70 | if errors.Is(err, consts.ErrNotFound) { 71 | code = http.StatusNotFound 72 | } else if errors.Is(err, consts.ErrIndexOutOfRange) { 73 | code = http.StatusBadRequest 74 | } else if errors.Is(err, consts.ErrAlreadyExists) { 75 | code = http.StatusConflict 76 | } else if errors.Is(err, consts.ErrForbidden) { 77 | code = http.StatusForbidden 78 | } else if errors.Is(err, consts.ErrInvalidArgument) { 79 | code = http.StatusBadRequest 80 | } 81 | c.JSON(code, Response{ 82 | Error: &Error{Message: err.Error()}, 83 | }) 84 | c.Abort() 85 | } 86 | 87 | // generateSessionID encodes the addr to a session ID, 88 | // which is used to identify the session. And then can be used to 89 | // parse the leader listening address back. 90 | func GenerateSessionID(addr string) string { 91 | return fmt.Sprintf("%s/%s", util.RandString(8), addr) 92 | } 93 | 94 | // extractAddrFromSessionID decodes the session ID to the addr. 95 | func ExtractAddrFromSessionID(sessionID string) string { 96 | parts := strings.Split(sessionID, "/") 97 | if len(parts) != 2 { 98 | // for the old session ID format, we use the addr as the session ID 99 | return sessionID 100 | } 101 | return parts[1] 102 | } 103 | -------------------------------------------------------------------------------- /server/helper/helper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package helper 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestGenerateSessionID(t *testing.T) { 30 | testAddr := "127.0.0.1:1234" 31 | sessionID := GenerateSessionID(testAddr) 32 | decodedAddr := ExtractAddrFromSessionID(sessionID) 33 | require.Equal(t, testAddr, decodedAddr) 34 | 35 | // old format 36 | require.Equal(t, testAddr, ExtractAddrFromSessionID(testAddr)) 37 | } 38 | -------------------------------------------------------------------------------- /server/route.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package server 21 | 22 | import ( 23 | "github.com/gin-gonic/gin" 24 | "github.com/prometheus/client_golang/prometheus/promhttp" 25 | 26 | "github.com/apache/kvrocks-controller/server/helper" 27 | 28 | "github.com/apache/kvrocks-controller/consts" 29 | "github.com/apache/kvrocks-controller/server/api" 30 | "github.com/apache/kvrocks-controller/server/middleware" 31 | ) 32 | 33 | func (srv *Server) initHandlers() { 34 | engine := srv.engine 35 | engine.Use(middleware.CollectMetrics, func(c *gin.Context) { 36 | c.Set(consts.ContextKeyStore, srv.store) 37 | c.Next() 38 | }, middleware.RedirectIfNotLeader) 39 | handler := api.NewHandler(srv.store) 40 | 41 | engine.Any("/debug/pprof/*profile", PProf) 42 | engine.GET("/metrics", gin.WrapH(promhttp.Handler())) 43 | engine.NoRoute(func(c *gin.Context) { 44 | helper.ResponseError(c, consts.ErrNotFound) 45 | c.Abort() 46 | }) 47 | 48 | apiV1 := engine.Group("/api/v1/") 49 | { 50 | raftAPI := apiV1.Group("raft") 51 | { 52 | raftAPI.Use(middleware.RequiredRaftEngine) 53 | raftAPI.POST("/peers", handler.Raft.UpdatePeer) 54 | raftAPI.GET("/peers", handler.Raft.ListPeers) 55 | } 56 | 57 | namespaces := apiV1.Group("namespaces") 58 | { 59 | namespaces.GET("", handler.Namespace.List) 60 | namespaces.GET("/:namespace", handler.Namespace.Exists) 61 | namespaces.POST("", handler.Namespace.Create) 62 | namespaces.DELETE("/:namespace", handler.Namespace.Remove) 63 | } 64 | 65 | clusters := namespaces.Group("/:namespace/clusters") 66 | { 67 | clusters.GET("", middleware.RequiredNamespace, handler.Cluster.List) 68 | clusters.POST("", middleware.RequiredNamespace, handler.Cluster.Create) 69 | clusters.POST("/:cluster/import", middleware.RequiredNamespace, handler.Cluster.Import) 70 | clusters.GET("/:cluster", middleware.RequiredCluster, handler.Cluster.Get) 71 | clusters.DELETE("/:cluster", middleware.RequiredCluster, handler.Cluster.Remove) 72 | clusters.POST("/:cluster/migrate", middleware.RequiredCluster, handler.Cluster.MigrateSlot) 73 | } 74 | 75 | shards := clusters.Group("/:cluster/shards") 76 | { 77 | shards.GET("", middleware.RequiredCluster, handler.Shard.List) 78 | shards.POST("", middleware.RequiredCluster, handler.Shard.Create) 79 | shards.GET("/:shard", middleware.RequiredClusterShard, handler.Shard.Get) 80 | shards.DELETE("/:shard", middleware.RequiredCluster, handler.Shard.Remove) 81 | shards.POST("/:shard/failover", middleware.RequiredClusterShard, handler.Shard.Failover) 82 | } 83 | 84 | nodes := shards.Group("/:shard/nodes") 85 | { 86 | nodes.GET("", middleware.RequiredClusterShard, handler.Node.List) 87 | nodes.POST("", middleware.RequiredClusterShard, handler.Node.Create) 88 | nodes.DELETE("/:id", middleware.RequiredClusterShard, handler.Node.Remove) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /store/cluster_mock_node.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package store 22 | 23 | import "context" 24 | 25 | // ClusterMockNode is a mock implementation of the Node interface, 26 | // it is used for testing purposes. 27 | type ClusterMockNode struct { 28 | *ClusterNode 29 | 30 | Sequence uint64 31 | } 32 | 33 | var _ Node = (*ClusterMockNode)(nil) 34 | 35 | func NewClusterMockNode() *ClusterMockNode { 36 | return &ClusterMockNode{ 37 | ClusterNode: NewClusterNode("", ""), 38 | } 39 | } 40 | 41 | func (mock *ClusterMockNode) GetClusterNodeInfo(ctx context.Context) (*ClusterNodeInfo, error) { 42 | return &ClusterNodeInfo{Sequence: mock.Sequence}, nil 43 | } 44 | 45 | func (mock *ClusterMockNode) GetClusterInfo(ctx context.Context) (*ClusterInfo, error) { 46 | return &ClusterInfo{}, nil 47 | } 48 | 49 | func (mock *ClusterMockNode) SyncClusterInfo(ctx context.Context, cluster *Cluster) error { 50 | return nil 51 | } 52 | 53 | func (mock *ClusterMockNode) Reset(ctx context.Context) error { 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /store/cluster_node_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package store 21 | 22 | import ( 23 | "context" 24 | "strings" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestClusterNode(t *testing.T) { 31 | ctx := context.Background() 32 | nodeAddr0 := "127.0.0.1:7770" 33 | nodeAddr1 := "127.0.0.1:7771" 34 | node0 := NewClusterNode(nodeAddr0, "") 35 | node1 := NewClusterNode(nodeAddr1, "") 36 | redisCli := node0.GetClient() 37 | 38 | defer func() { 39 | require.NoError(t, redisCli.FlushAll(ctx).Err()) 40 | require.NoError(t, redisCli.Do(ctx, "COMPACT").Err()) 41 | require.NoError(t, redisCli.Do(ctx, "CLUSTER", "RESET").Err()) 42 | }() 43 | 44 | t.Run("Check the cluster mode", func(t *testing.T) { 45 | _, err := node0.CheckClusterMode(ctx) 46 | require.NoError(t, err) 47 | 48 | require.NoError(t, redisCli.Do(ctx, "CLUSTER", "RESET").Err()) 49 | // set the cluster topology 50 | cluster := &Cluster{Shards: Shards{ 51 | {Nodes: []Node{node0}, SlotRanges: []SlotRange{ 52 | {Start: 0, Stop: 100}, 53 | {Start: 102, Stop: 300}, 54 | {Start: 302, Stop: 16383}, 55 | }}, 56 | {Nodes: []Node{node1}, SlotRanges: []SlotRange{}}, 57 | }} 58 | 59 | cluster.Version.Store(1) 60 | require.NoError(t, node0.SyncClusterInfo(ctx, cluster)) 61 | clusterInfo, err := node0.GetClusterInfo(ctx) 62 | require.NoError(t, err) 63 | require.EqualValues(t, 1, clusterInfo.CurrentEpoch) 64 | }) 65 | 66 | t.Run("Check the cluster node0 info", func(t *testing.T) { 67 | require.NoError(t, redisCli.Set(ctx, "foo", "bar", 0).Err()) 68 | info, err := node0.GetClusterNodeInfo(ctx) 69 | require.NoError(t, err) 70 | require.True(t, info.Sequence > 0) 71 | }) 72 | 73 | t.Run("Parse the cluster node info", func(t *testing.T) { 74 | clusterNodesStr, err := node0.GetClusterNodesString(ctx) 75 | require.NoError(t, err) 76 | clusterNodes, err := ParseCluster(clusterNodesStr) 77 | require.NoError(t, err) 78 | require.EqualValues(t, 1, clusterNodes.Version.Load()) 79 | require.Len(t, clusterNodes.Shards, 2) 80 | require.Len(t, clusterNodes.Shards[0].Nodes, 1) 81 | require.EqualValues(t, []SlotRange{ 82 | {Start: 0, Stop: 100}, 83 | {Start: 102, Stop: 300}, 84 | {Start: 302, Stop: 16383}, 85 | }, clusterNodes.Shards[0].SlotRanges) 86 | require.EqualValues(t, nodeAddr0, clusterNodes.Shards[0].Nodes[0].Addr()) 87 | require.EqualValues(t, node0.ID(), clusterNodes.Shards[0].Nodes[0].ID()) 88 | 89 | // Ensure empty slot range is allowed in the cluster node info 90 | require.Len(t, clusterNodes.Shards[1].Nodes, 1) 91 | require.EqualValues(t, []SlotRange{}, clusterNodes.Shards[1].SlotRanges) 92 | require.EqualValues(t, nodeAddr1, clusterNodes.Shards[1].Nodes[0].Addr()) 93 | }) 94 | } 95 | 96 | func TestNodeInfo_Validate(t *testing.T) { 97 | node := &ClusterNode{} 98 | require.EqualError(t, node.Validate(), "node id shouldn't be empty") 99 | node.id = "1234" 100 | require.EqualError(t, node.Validate(), "the length of node id must be 40") 101 | node.id = strings.Repeat("1", NodeIDLen) 102 | require.EqualError(t, node.Validate(), "node role should be 'master' or 'slave'") 103 | node.role = RoleMaster 104 | node.addr = "1.2.3.4" 105 | require.NoError(t, node.Validate()) 106 | } 107 | -------------------------------------------------------------------------------- /store/cluster_shard_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package store 22 | 23 | import ( 24 | "sort" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestShard_HasOverlap(t *testing.T) { 31 | shard := NewShard() 32 | slotRange := SlotRange{Start: 0, Stop: 100} 33 | shard.SlotRanges = append(shard.SlotRanges, slotRange) 34 | require.True(t, shard.HasOverlap(slotRange)) 35 | require.True(t, shard.HasOverlap(SlotRange{Start: 50, Stop: 150})) 36 | require.False(t, shard.HasOverlap(SlotRange{Start: 101, Stop: 150})) 37 | } 38 | 39 | func TestShard_Sort(t *testing.T) { 40 | shard0 := NewShard() 41 | shard0.SlotRanges = []SlotRange{{Start: 201, Stop: 300}} 42 | shard1 := NewShard() 43 | shard1.SlotRanges = []SlotRange{{Start: 0, Stop: 400}} 44 | shard2 := NewShard() 45 | shard2.SlotRanges = []SlotRange{{Start: 101, Stop: 500}} 46 | shard3 := NewShard() 47 | shard3.SlotRanges = []SlotRange{} 48 | shards := Shards{shard0, shard1, shard2, shard3} 49 | sort.Sort(shards) 50 | require.EqualValues(t, 0, shards[0].SlotRanges[0].Start) 51 | require.EqualValues(t, 101, shards[1].SlotRanges[0].Start) 52 | require.EqualValues(t, 201, shards[2].SlotRanges[0].Start) 53 | require.EqualValues(t, 0, len(shards[3].SlotRanges)) 54 | } 55 | 56 | func TestShard_IsServicing(t *testing.T) { 57 | var err error 58 | shard := NewShard() 59 | shard.TargetShardIndex = 0 60 | shard.MigratingSlot = &MigratingSlot{IsMigrating: false} 61 | require.False(t, shard.IsServicing()) 62 | 63 | shard.TargetShardIndex = 0 64 | shard.MigratingSlot = nil 65 | require.False(t, shard.IsServicing()) 66 | 67 | shard.TargetShardIndex = 0 68 | slotRange, err := NewSlotRange(1, 1) 69 | require.Nil(t, err) 70 | shard.MigratingSlot = FromSlotRange(slotRange) 71 | require.True(t, shard.IsServicing()) 72 | 73 | shard.TargetShardIndex = -1 74 | shard.MigratingSlot = &MigratingSlot{IsMigrating: false} 75 | shard.SlotRanges = []SlotRange{{Start: 0, Stop: 100}} 76 | require.True(t, shard.IsServicing()) 77 | 78 | shard.SlotRanges = []SlotRange{{Start: -1, Stop: -1}} 79 | require.False(t, shard.IsServicing()) 80 | } 81 | -------------------------------------------------------------------------------- /store/cluster_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package store 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/require" 28 | 29 | "github.com/apache/kvrocks-controller/consts" 30 | ) 31 | 32 | func TestCluster_Clone(t *testing.T) { 33 | cluster, err := NewCluster("test", []string{"node1", "node2", "node3"}, 1) 34 | require.NoError(t, err) 35 | 36 | clusterCopy := cluster.Clone() 37 | require.Equal(t, cluster.Name, clusterCopy.Name) 38 | require.Equal(t, cluster.Shards, clusterCopy.Shards) 39 | } 40 | 41 | func TestCluster_FindIndexShardBySlot(t *testing.T) { 42 | cluster, err := NewCluster("test", []string{"node1", "node2", "node3"}, 1) 43 | require.NoError(t, err) 44 | 45 | slotRange, err := NewSlotRange(0, 0) 46 | require.NoError(t, err) 47 | shard, err := cluster.findShardIndexBySlot(slotRange) 48 | require.NoError(t, err) 49 | require.Equal(t, 0, shard) 50 | 51 | slotRange, err = NewSlotRange(MaxSlotID/3+1, MaxSlotID/3+1) 52 | require.NoError(t, err) 53 | shard, err = cluster.findShardIndexBySlot(slotRange) 54 | require.NoError(t, err) 55 | require.Equal(t, 1, shard) 56 | 57 | slotRange, err = NewSlotRange(MaxSlotID, MaxSlotID) 58 | require.NoError(t, err) 59 | shard, err = cluster.findShardIndexBySlot(slotRange) 60 | require.NoError(t, err) 61 | require.Equal(t, 2, shard) 62 | } 63 | 64 | func TestCluster_PromoteNewMaster(t *testing.T) { 65 | shard := NewShard() 66 | shard.SlotRanges = []SlotRange{{Start: 0, Stop: 1023}} 67 | 68 | node0 := NewClusterMockNode() 69 | node0.SetRole(RoleMaster) 70 | 71 | node1 := NewClusterMockNode() 72 | node1.SetRole(RoleSlave) 73 | node1.Sequence = 200 74 | 75 | node2 := NewClusterMockNode() 76 | node2.SetRole(RoleSlave) 77 | node2.Sequence = 100 78 | 79 | node3 := NewClusterMockNode() 80 | node3.SetRole(RoleSlave) 81 | node3.Sequence = 300 82 | 83 | shard.Nodes = []Node{node0} 84 | cluster := &Cluster{ 85 | Shards: Shards{shard}, 86 | } 87 | 88 | ctx := context.Background() 89 | _, err := cluster.PromoteNewMaster(ctx, -1, node0.ID(), "") 90 | require.ErrorIs(t, err, consts.ErrIndexOutOfRange) 91 | _, err = cluster.PromoteNewMaster(ctx, 1, node0.ID(), "") 92 | require.ErrorIs(t, err, consts.ErrIndexOutOfRange) 93 | _, err = cluster.PromoteNewMaster(ctx, 0, node0.ID(), "") 94 | require.ErrorIs(t, err, consts.ErrShardNoReplica) 95 | 96 | shard.Nodes = append(shard.Nodes, node1, node2, node3) 97 | _, err = cluster.PromoteNewMaster(ctx, 0, node1.ID(), "") 98 | require.ErrorIs(t, err, consts.ErrNodeIsNotMaster) 99 | 100 | newMasterID, err := cluster.PromoteNewMaster(ctx, 0, node0.ID(), "") 101 | require.NoError(t, err) 102 | require.Equal(t, node3.ID(), newMasterID) 103 | 104 | // test preferredNodeID 105 | newMasterID, err = cluster.PromoteNewMaster(ctx, 0, node3.ID(), node2.ID()) 106 | require.NoError(t, err) 107 | require.Equal(t, node2.ID(), newMasterID) 108 | } 109 | -------------------------------------------------------------------------------- /store/engine/consul/consul_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package consul 21 | 22 | import ( 23 | "context" 24 | "testing" 25 | "time" 26 | 27 | "github.com/apache/kvrocks-controller/util" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | const addr = "127.0.0.1:8500" 32 | 33 | func TestBasicOperations(t *testing.T) { 34 | id := util.RandString(40) 35 | testElectPath := util.RandString(32) 36 | print(testElectPath) 37 | persist, err := New(id, &Config{ 38 | ElectPath: testElectPath, 39 | Addrs: []string{addr}, 40 | }) 41 | require.NoError(t, err) 42 | defer persist.Close() 43 | go func() { 44 | for range persist.LeaderChange() { 45 | // do nothing 46 | } 47 | }() 48 | 49 | ctx := context.Background() 50 | keys := []string{"/a/b/c0", "/a/b/c1", "/a/b/c2"} 51 | value := []byte("v") 52 | for _, key := range keys { 53 | require.NoError(t, persist.Set(ctx, key, value)) 54 | gotValue, err := persist.Get(ctx, key) 55 | require.NoError(t, err) 56 | require.Equal(t, value, gotValue) 57 | } 58 | entries, err := persist.List(ctx, "/a/b") 59 | require.NoError(t, err) 60 | require.Equal(t, len(keys), len(entries)) 61 | for _, key := range keys { 62 | require.NoError(t, persist.Delete(ctx, key)) 63 | } 64 | } 65 | 66 | func TestElect(t *testing.T) { 67 | endpoints := []string{addr} 68 | 69 | testElectPath := util.RandString(32) 70 | id0 := util.RandString(40) 71 | node0, err := New(id0, &Config{ 72 | ElectPath: testElectPath, 73 | Addrs: endpoints, 74 | }) 75 | require.NoError(t, err) 76 | require.Eventuallyf(t, func() bool { 77 | return node0.Leader() == node0.myID 78 | }, 10*time.Second, 100*time.Millisecond, "node0 should be the leader") 79 | 80 | id1 := util.RandString(40) 81 | node1, err := New(id1, &Config{ 82 | ElectPath: testElectPath, 83 | Addrs: endpoints, 84 | }) 85 | require.NoError(t, err) 86 | require.Eventuallyf(t, func() bool { 87 | return node1.Leader() == node0.myID 88 | }, 10*time.Second, 100*time.Millisecond, "node1's leader should be the node0") 89 | 90 | go func() { 91 | for { 92 | select { 93 | case <-node0.LeaderChange(): 94 | // do nothing 95 | case <-node1.LeaderChange(): 96 | // do nothing 97 | } 98 | } 99 | }() 100 | 101 | require.NoError(t, node0.Close()) 102 | 103 | require.Eventuallyf(t, func() bool { 104 | return node1.Leader() == node1.myID 105 | }, 25*time.Second, 100*time.Millisecond, "node1 should be the leader") 106 | } 107 | -------------------------------------------------------------------------------- /store/engine/engine.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package engine 22 | 23 | import ( 24 | "context" 25 | ) 26 | 27 | type Entry struct { 28 | Key string `json:"key"` 29 | Value []byte `json:"value"` 30 | } 31 | 32 | type Engine interface { 33 | ID() string 34 | Leader() string 35 | LeaderChange() <-chan bool 36 | IsReady(ctx context.Context) bool 37 | 38 | Get(ctx context.Context, key string) ([]byte, error) 39 | Exists(ctx context.Context, key string) (bool, error) 40 | Set(ctx context.Context, key string, value []byte) error 41 | Delete(ctx context.Context, key string) error 42 | List(ctx context.Context, prefix string) ([]Entry, error) 43 | 44 | Close() error 45 | } 46 | -------------------------------------------------------------------------------- /store/engine/engine_inmemory.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package engine 21 | 22 | import ( 23 | "context" 24 | "strings" 25 | "sync" 26 | 27 | "github.com/apache/kvrocks-controller/consts" 28 | ) 29 | 30 | var _ Engine = (*Mock)(nil) 31 | 32 | type Mock struct { 33 | mu sync.Mutex 34 | values map[string]string 35 | } 36 | 37 | func NewMock() *Mock { 38 | return &Mock{ 39 | values: make(map[string]string), 40 | } 41 | } 42 | 43 | func (m *Mock) Get(_ context.Context, key string) ([]byte, error) { 44 | m.mu.Lock() 45 | defer m.mu.Unlock() 46 | v, ok := m.values[key] 47 | if !ok { 48 | return nil, consts.ErrNotFound 49 | } 50 | return []byte(v), nil 51 | } 52 | 53 | func (m *Mock) Exists(_ context.Context, key string) (bool, error) { 54 | m.mu.Lock() 55 | defer m.mu.Unlock() 56 | _, ok := m.values[key] 57 | return ok, nil 58 | } 59 | 60 | func (m *Mock) Set(_ context.Context, key string, value []byte) error { 61 | m.mu.Lock() 62 | defer m.mu.Unlock() 63 | m.values[key] = string(value) 64 | return nil 65 | } 66 | 67 | func (m *Mock) Delete(_ context.Context, key string) error { 68 | m.mu.Lock() 69 | defer m.mu.Unlock() 70 | delete(m.values, key) 71 | return nil 72 | } 73 | 74 | func (m *Mock) List(_ context.Context, prefix string) ([]Entry, error) { 75 | m.mu.Lock() 76 | defer m.mu.Unlock() 77 | 78 | exists := make(map[string]bool, 0) 79 | var entries []Entry 80 | for k, v := range m.values { 81 | if strings.HasPrefix(k, prefix) { 82 | k = strings.Trim(strings.TrimPrefix(k, prefix), "/") 83 | fields := strings.SplitN(k, "/", 2) 84 | if len(fields) == 2 { 85 | // only list the first level 86 | k = fields[0] 87 | } 88 | if _, ok := exists[k]; ok { 89 | continue 90 | } 91 | exists[k] = true 92 | entries = append(entries, Entry{ 93 | Key: k, 94 | Value: []byte(v), 95 | }) 96 | } 97 | } 98 | return entries, nil 99 | } 100 | 101 | func (m *Mock) Close() error { 102 | return nil 103 | } 104 | 105 | func (m *Mock) ID() string { 106 | return "mock_store_engine" 107 | } 108 | 109 | func (m *Mock) Leader() string { 110 | return "mock_store_engine" 111 | } 112 | 113 | func (m *Mock) LeaderChange() <-chan bool { 114 | return make(chan bool) 115 | } 116 | 117 | func (m *Mock) IsReady(_ context.Context) bool { 118 | return true 119 | } 120 | -------------------------------------------------------------------------------- /store/engine/etcd/etcd_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package etcd 21 | 22 | import ( 23 | "context" 24 | "testing" 25 | "time" 26 | 27 | "github.com/apache/kvrocks-controller/util" 28 | 29 | "github.com/stretchr/testify/require" 30 | ) 31 | 32 | const addr = "127.0.0.1:2379" 33 | 34 | func TestBasicOperations(t *testing.T) { 35 | id := util.RandString(40) 36 | testElectPath := util.RandString(32) 37 | persist, err := New(id, &Config{ 38 | ElectPath: testElectPath, 39 | Addrs: []string{addr}, 40 | }) 41 | require.NoError(t, err) 42 | defer persist.Close() 43 | go func() { 44 | for range persist.LeaderChange() { 45 | // do nothing 46 | } 47 | }() 48 | 49 | ctx := context.Background() 50 | keys := []string{"/a/b/c0", "/a/b/c1", "/a/b/c2"} 51 | value := []byte("v") 52 | for _, key := range keys { 53 | require.NoError(t, persist.Set(ctx, key, value)) 54 | gotValue, err := persist.Get(ctx, key) 55 | require.NoError(t, err) 56 | require.Equal(t, value, gotValue) 57 | } 58 | entries, err := persist.List(ctx, "/a/b") 59 | require.NoError(t, err) 60 | require.Equal(t, len(keys), len(entries)) 61 | for _, key := range keys { 62 | require.NoError(t, persist.Delete(ctx, key)) 63 | } 64 | } 65 | 66 | func TestElect(t *testing.T) { 67 | endpoints := []string{addr} 68 | 69 | testElectPath := util.RandString(32) 70 | id0 := util.RandString(40) 71 | node0, err := New(id0, &Config{ 72 | ElectPath: testElectPath, 73 | Addrs: endpoints, 74 | }) 75 | require.NoError(t, err) 76 | require.Eventuallyf(t, func() bool { 77 | return node0.Leader() == node0.myID 78 | }, 10*time.Second, 100*time.Millisecond, "node0 should be the leader") 79 | 80 | id1 := util.RandString(40) 81 | node1, err := New(id1, &Config{ 82 | ElectPath: testElectPath, 83 | Addrs: endpoints, 84 | }) 85 | require.NoError(t, err) 86 | require.Eventuallyf(t, func() bool { 87 | return node1.Leader() == node0.myID 88 | }, 10*time.Second, 100*time.Millisecond, "node1's leader should be the node0") 89 | 90 | go func() { 91 | for { 92 | select { 93 | case <-node0.LeaderChange(): 94 | // do nothing 95 | case <-node1.LeaderChange(): 96 | // do nothing 97 | } 98 | } 99 | }() 100 | 101 | require.NoError(t, node0.Close()) 102 | 103 | require.Eventuallyf(t, func() bool { 104 | return node1.Leader() == node1.myID 105 | }, 15*time.Second, 100*time.Millisecond, "node1 should be the leader") 106 | } 107 | -------------------------------------------------------------------------------- /store/engine/postgresql/postgresql_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package postgresql 21 | 22 | import ( 23 | "context" 24 | "testing" 25 | "time" 26 | 27 | "github.com/apache/kvrocks-controller/util" 28 | 29 | "github.com/stretchr/testify/require" 30 | ) 31 | 32 | const ( 33 | addr = "127.0.0.1:5432" 34 | notifyChannel = "lock_change" 35 | username = "postgres" 36 | password = "postgres" 37 | dbName = "testdb" 38 | ) 39 | 40 | func TestBasicOperations(t *testing.T) { 41 | id := util.RandString(40) 42 | testElectPath := util.RandString(32) 43 | persist, err := New(id, &Config{ 44 | Username: username, 45 | Password: password, 46 | DBName: dbName, 47 | NotifyChannel: notifyChannel, 48 | ElectPath: testElectPath, 49 | Addrs: []string{addr}, 50 | }) 51 | require.NoError(t, err) 52 | defer persist.Close() 53 | go func() { 54 | for range persist.LeaderChange() { 55 | // do nothing 56 | } 57 | }() 58 | 59 | ctx := context.Background() 60 | keys := []string{"/a/b/c0", "/a/b/c1", "/a/b/c2"} 61 | value := []byte("v") 62 | for _, key := range keys { 63 | require.NoError(t, persist.Set(ctx, key, value)) 64 | gotValue, err := persist.Get(ctx, key) 65 | require.NoError(t, err) 66 | require.Equal(t, value, gotValue) 67 | } 68 | entries, err := persist.List(ctx, "/a/b") 69 | require.NoError(t, err) 70 | require.Equal(t, len(keys), len(entries)) 71 | for _, key := range keys { 72 | require.NoError(t, persist.Delete(ctx, key)) 73 | } 74 | } 75 | 76 | func TestElect(t *testing.T) { 77 | endpoints := []string{addr} 78 | 79 | testElectPath := util.RandString(32) 80 | id0 := util.RandString(40) 81 | node0, err := New(id0, &Config{ 82 | Username: username, 83 | Password: password, 84 | DBName: dbName, 85 | NotifyChannel: notifyChannel, 86 | ElectPath: testElectPath, 87 | Addrs: endpoints, 88 | }) 89 | require.NoError(t, err) 90 | require.Eventuallyf(t, func() bool { 91 | return node0.Leader() == node0.myID 92 | }, 10*time.Second, 100*time.Millisecond, "node0 should be the leader") 93 | 94 | id1 := util.RandString(40) 95 | node1, err := New(id1, &Config{ 96 | Username: username, 97 | Password: password, 98 | DBName: dbName, 99 | NotifyChannel: notifyChannel, 100 | ElectPath: testElectPath, 101 | Addrs: endpoints, 102 | }) 103 | require.NoError(t, err) 104 | require.Eventuallyf(t, func() bool { 105 | return node1.Leader() == node0.myID 106 | }, 10*time.Second, 100*time.Millisecond, "node1's leader should be the node0") 107 | 108 | go func() { 109 | for { 110 | select { 111 | case <-node0.LeaderChange(): 112 | // do nothing 113 | case <-node1.LeaderChange(): 114 | // do nothing 115 | } 116 | } 117 | }() 118 | 119 | require.NoError(t, node0.Close()) 120 | 121 | require.Eventuallyf(t, func() bool { 122 | return node1.Leader() == node1.myID 123 | }, 15*time.Second, 100*time.Millisecond, "node1 should be the leader") 124 | } 125 | -------------------------------------------------------------------------------- /store/engine/raft/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package raft 22 | 23 | import ( 24 | "errors" 25 | "strings" 26 | ) 27 | 28 | const ( 29 | ClusterStateNew = "new" 30 | ClusterStateExisting = "existing" 31 | ) 32 | 33 | type Config struct { 34 | // ID is the identity of the local raft. ID cannot be 0. 35 | ID uint64 `yaml:"id"` 36 | // DataDir is the directory to store the raft data which includes snapshot and WALs. 37 | DataDir string `yaml:"data_dir"` 38 | // ClusterState is the state of the cluster, can be one of "new" and "existing". 39 | ClusterState string `yaml:"cluster_state"` 40 | // Peers is the list of raft peers. 41 | Peers []string `yaml:"peers"` 42 | // HeartbeatSeconds is the interval to send heartbeat message. Default is 2 seconds. 43 | HeartbeatSeconds int `yaml:"heartbeat_seconds"` 44 | // ElectionSeconds is the interval to start an election. Default is 10 * HeartBeat. 45 | ElectionSeconds int `yaml:"election_seconds"` 46 | } 47 | 48 | func (c *Config) validate() error { 49 | if c.ID == 0 { 50 | return errors.New("ID cannot be 0") 51 | } 52 | if len(c.Peers) == 0 { 53 | return errors.New("peers cannot be empty") 54 | } 55 | if c.ID > uint64(len(c.Peers)) { 56 | return errors.New("ID cannot be greater than the number of peers") 57 | } 58 | clusterState := strings.ToLower(c.ClusterState) 59 | if clusterState != ClusterStateNew && clusterState != ClusterStateExisting { 60 | return errors.New("cluster state must be one of [new, existing]") 61 | } 62 | return nil 63 | } 64 | 65 | func (c *Config) init() { 66 | c.ClusterState = ClusterStateNew 67 | if c.DataDir == "" { 68 | c.DataDir = "." 69 | } 70 | if c.HeartbeatSeconds == 0 { 71 | c.HeartbeatSeconds = 2 72 | } 73 | if c.ElectionSeconds == 0 { 74 | c.ElectionSeconds = c.HeartbeatSeconds * 10 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /store/engine/raft/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package raft 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestConfig_Validate(t *testing.T) { 30 | c := &Config{} 31 | c.init() 32 | 33 | // missing ID 34 | require.ErrorContains(t, c.validate(), "ID cannot be 0") 35 | // missing peers 36 | c.ID = 1 37 | require.ErrorContains(t, c.validate(), "peers cannot be empty") 38 | // valid 39 | c.Peers = []string{"http://127.0.0.1:12345"} 40 | require.NoError(t, c.validate()) 41 | // ID greater than the number of peers 42 | c.ID = 2 43 | require.ErrorContains(t, c.validate(), "ID cannot be greater than the number of peers") 44 | 45 | c.ID = 1 46 | c.ClusterState = "invalid" 47 | require.ErrorContains(t, c.validate(), "cluster state must be one of [new, existing]") 48 | c.ClusterState = ClusterStateNew 49 | require.NoError(t, c.validate()) 50 | } 51 | 52 | func TestConfig_Init(t *testing.T) { 53 | c := &Config{} 54 | c.init() 55 | require.Equal(t, ".", c.DataDir) 56 | require.Equal(t, 2, c.HeartbeatSeconds) 57 | require.Equal(t, 20, c.ElectionSeconds) 58 | 59 | c.DataDir = "/tmp" 60 | c.HeartbeatSeconds = 3 61 | c.ElectionSeconds = 30 62 | c.init() 63 | require.Equal(t, "/tmp", c.DataDir) 64 | require.Equal(t, 3, c.HeartbeatSeconds) 65 | require.Equal(t, 30, c.ElectionSeconds) 66 | } 67 | -------------------------------------------------------------------------------- /store/engine/raft/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package raft 22 | 23 | import ( 24 | "go.etcd.io/etcd/raft/v3" 25 | "go.uber.org/zap" 26 | ) 27 | 28 | var _ raft.Logger = &Logger{} 29 | 30 | // Logger is a wrapper around zap.SugaredLogger to implement the raft.Logger interface. 31 | type Logger struct { 32 | *zap.SugaredLogger 33 | } 34 | 35 | func (r Logger) Warning(v ...interface{}) { 36 | r.SugaredLogger.Warn(v...) 37 | } 38 | 39 | func (r Logger) Warningf(format string, v ...interface{}) { 40 | r.SugaredLogger.Warnf(format, v...) 41 | } 42 | 43 | func (r Logger) Debug(v ...interface{}) { 44 | r.SugaredLogger.Debug(v...) 45 | } 46 | 47 | func (r Logger) Debugf(format string, v ...interface{}) { 48 | r.SugaredLogger.Debugf(format, v...) 49 | } 50 | 51 | func (r Logger) Error(v ...interface{}) { 52 | r.SugaredLogger.Error(v...) 53 | } 54 | 55 | func (r Logger) Errorf(format string, v ...interface{}) { 56 | r.SugaredLogger.Errorf(format, v...) 57 | } 58 | 59 | func (r Logger) Info(v ...interface{}) { 60 | r.SugaredLogger.Info(v...) 61 | } 62 | 63 | func (r Logger) Infof(format string, v ...interface{}) { 64 | r.SugaredLogger.Infof(format, v...) 65 | } 66 | 67 | func (r Logger) Fatal(v ...interface{}) { 68 | r.SugaredLogger.Fatal(v...) 69 | } 70 | 71 | func (r Logger) Fatalf(format string, v ...interface{}) { 72 | r.SugaredLogger.Fatalf(format, v...) 73 | } 74 | 75 | func (r Logger) Panic(v ...interface{}) { 76 | r.SugaredLogger.Panic(v...) 77 | } 78 | 79 | func (r Logger) Panicf(format string, v ...interface{}) { 80 | r.SugaredLogger.Panicf(format, v...) 81 | } 82 | -------------------------------------------------------------------------------- /store/engine/raft/store_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | 21 | package raft 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "os" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/require" 30 | "go.etcd.io/etcd/raft/v3/raftpb" 31 | ) 32 | 33 | func TestDataStore(t *testing.T) { 34 | dir := "/tmp/kvrocks/raft/test-datastore" 35 | store := NewDataStore(dir) 36 | require.NotNil(t, store) 37 | 38 | defer func() { 39 | store.Close() 40 | os.RemoveAll(dir) 41 | }() 42 | 43 | _, err := store.replayWAL() 44 | require.NoError(t, err) 45 | 46 | t.Run("reply WAL from the disk", func(t *testing.T) { 47 | entries := make([]raftpb.Entry, 0) 48 | for i := 0; i < 3; i++ { 49 | payload, err := json.Marshal(Event{Op: opSet, Key: fmt.Sprintf("key-%d", i), Value: []byte(fmt.Sprintf("value-%d", i))}) 50 | require.NoError(t, err) 51 | entries = append(entries, raftpb.Entry{Term: 1, Index: uint64(i + 1), Type: raftpb.EntryNormal, Data: payload}) 52 | } 53 | require.NoError(t, store.wal.Save(raftpb.HardState{Term: 1, Vote: 1}, entries)) 54 | store.Close() 55 | 56 | store = NewDataStore(dir) 57 | snapshot, err := store.replayWAL() 58 | require.NoError(t, err) 59 | require.NotNil(t, snapshot) 60 | 61 | firstIndex, err := store.raftStorage.FirstIndex() 62 | require.NoError(t, err) 63 | require.EqualValues(t, 1, firstIndex) 64 | 65 | lastIndex, err := store.raftStorage.LastIndex() 66 | require.NoError(t, err) 67 | require.EqualValues(t, 3, lastIndex) 68 | 69 | term, err := store.raftStorage.Term(1) 70 | require.NoError(t, err) 71 | require.EqualValues(t, 1, term) 72 | 73 | for i := 0; i < 3; i++ { 74 | v, err := store.Get(fmt.Sprintf("key-%d", i)) 75 | require.NoError(t, err) 76 | require.Equal(t, []byte(fmt.Sprintf("value-%d", i)), v) 77 | } 78 | }) 79 | 80 | t.Run("Basic GET/SET/DELETE/LIST", func(t *testing.T) { 81 | store.Set("bar-1", []byte("v1")) 82 | store.Set("bar-2", []byte("v2")) 83 | store.Set("baz-3", []byte("v3")) 84 | store.Set("ba-4", []byte("v4")) 85 | store.Set("foo", []byte("v5")) 86 | 87 | v, err := store.Get("bar-2") 88 | require.NoError(t, err) 89 | require.Equal(t, []byte("v2"), v) 90 | 91 | entries := store.List("bar") 92 | require.Len(t, entries, 2) 93 | 94 | entries = store.List("baz") 95 | require.Len(t, entries, 1) 96 | 97 | entries = store.List("ba") 98 | require.Len(t, entries, 4) 99 | 100 | entries = store.List("bar") 101 | require.Len(t, entries, 2) 102 | 103 | entries = store.List("fo") 104 | require.Len(t, entries, 1) 105 | 106 | store.Delete("bar-2") 107 | _, err = store.Get("bar-2") 108 | require.ErrorIs(t, err, ErrKeyNotFound) 109 | 110 | entries = store.List("bar") 111 | require.Len(t, entries, 1) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /store/engine/zookeeper/zookeeper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package zookeeper 21 | 22 | import ( 23 | "context" 24 | "testing" 25 | "time" 26 | 27 | "github.com/apache/kvrocks-controller/util" 28 | 29 | "github.com/stretchr/testify/require" 30 | ) 31 | 32 | const addr = "127.0.0.1:2181" 33 | 34 | func TestBasicOperations(t *testing.T) { 35 | id := util.RandString(40) 36 | testElectPath := "/" + util.RandString(8) + "/" + util.RandString(8) 37 | persist, err := New(id, &Config{ 38 | ElectPath: testElectPath, 39 | Addrs: []string{addr}, 40 | }) 41 | require.NoError(t, err) 42 | defer persist.Close() 43 | go func() { 44 | for range persist.LeaderChange() { 45 | // do nothing 46 | } 47 | }() 48 | 49 | ctx := context.Background() 50 | keys := []string{"/a/b/c0", "/a/b/c1", "/a/b/c2"} 51 | value := []byte("v") 52 | for _, key := range keys { 53 | require.NoError(t, persist.Set(ctx, key, value)) 54 | gotValue, err := persist.Get(ctx, key) 55 | require.NoError(t, err) 56 | require.Equal(t, value, gotValue) 57 | } 58 | entries, err := persist.List(ctx, "/a/b") 59 | require.NoError(t, err) 60 | require.Equal(t, len(keys), len(entries)) 61 | for _, key := range keys { 62 | require.NoError(t, persist.Delete(ctx, key)) 63 | } 64 | } 65 | 66 | func TestElect(t *testing.T) { 67 | endpoints := []string{addr} 68 | 69 | testElectPath := "/" + util.RandString(8) + "/" + util.RandString(8) 70 | id0 := util.RandString(40) 71 | node0, err := New(id0, &Config{ 72 | ElectPath: testElectPath, 73 | Addrs: endpoints, 74 | }) 75 | require.NoError(t, err) 76 | require.Eventuallyf(t, func() bool { 77 | return node0.Leader() == node0.myID 78 | }, 10*time.Second, 100*time.Millisecond, "node0 should be the leader") 79 | 80 | id1 := util.RandString(40) 81 | node1, err := New(id1, &Config{ 82 | ElectPath: testElectPath, 83 | Addrs: endpoints, 84 | }) 85 | require.NoError(t, err) 86 | require.Eventuallyf(t, func() bool { 87 | return node1.Leader() == node0.myID 88 | }, 10*time.Second, 100*time.Millisecond, "node1's leader should be the node0") 89 | 90 | go func() { 91 | for { 92 | select { 93 | case <-node0.LeaderChange(): 94 | // do nothing 95 | case <-node1.LeaderChange(): 96 | // do nothing 97 | } 98 | } 99 | }() 100 | 101 | require.NoError(t, node0.Close()) 102 | 103 | require.Eventuallyf(t, func() bool { 104 | return node1.Leader() == node1.myID 105 | }, 15*time.Second, 100*time.Millisecond, "node1 should be the leader") 106 | require.NoError(t, node1.Close()) 107 | } 108 | -------------------------------------------------------------------------------- /store/event.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package store 21 | 22 | type EventType int 23 | type Command int 24 | 25 | const ( 26 | EventNamespace EventType = iota + 1 27 | EventCluster 28 | ) 29 | 30 | const ( 31 | CommandCreate = iota + 1 32 | CommandUpdate = iota + 1 33 | CommandRemove 34 | ) 35 | 36 | type EventPayload struct { 37 | Namespace string 38 | Cluster string 39 | Type EventType 40 | Command Command 41 | } 42 | -------------------------------------------------------------------------------- /store/helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package store 21 | 22 | import ( 23 | "fmt" 24 | ) 25 | 26 | const nsPrefix = "/kvrocks/metadata" 27 | 28 | func appendPrefix(ns string) string { 29 | return nsPrefix + "/" + ns 30 | } 31 | 32 | func buildClusterPrefix(ns string) string { 33 | return fmt.Sprintf("%s/%s/cluster", nsPrefix, ns) 34 | } 35 | 36 | func buildClusterKey(ns, cluster string) string { 37 | return fmt.Sprintf("%s/%s", buildClusterPrefix(ns), cluster) 38 | } 39 | -------------------------------------------------------------------------------- /store/store_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package store 21 | 22 | import ( 23 | "context" 24 | "testing" 25 | 26 | "github.com/stretchr/testify/require" 27 | 28 | "github.com/apache/kvrocks-controller/consts" 29 | "github.com/apache/kvrocks-controller/store/engine" 30 | ) 31 | 32 | func TestClusterStore(t *testing.T) { 33 | ctx := context.Background() 34 | store := NewClusterStore(engine.NewMock()) 35 | 36 | t.Run("create/get/list/delete namespace", func(t *testing.T) { 37 | namespaces := []string{"ns0", "ns1", "ns2"} 38 | for _, ns := range namespaces { 39 | err := store.CreateNamespace(ctx, ns) 40 | require.NoError(t, err) 41 | require.ErrorIs(t, store.CreateNamespace(ctx, ns), consts.ErrAlreadyExists) 42 | exists, err := store.ExistsNamespace(ctx, ns) 43 | require.NoError(t, err) 44 | require.True(t, exists) 45 | } 46 | 47 | exists, err := store.ExistsNamespace(ctx, "not-exits-ns") 48 | require.NoError(t, err) 49 | require.False(t, exists) 50 | 51 | gotNamespaces, err := store.ListNamespace(ctx) 52 | require.NoError(t, err) 53 | require.ElementsMatch(t, namespaces, gotNamespaces) 54 | 55 | for _, ns := range namespaces { 56 | err := store.RemoveNamespace(ctx, ns) 57 | require.NoError(t, err) 58 | exists, err := store.ExistsNamespace(ctx, ns) 59 | require.NoError(t, err) 60 | require.False(t, exists) 61 | } 62 | }) 63 | 64 | t.Run("create/get/list/delete cluster", func(t *testing.T) { 65 | ns := "ns0" 66 | cluster0 := &Cluster{Name: "cluster0", Shards: Shards{NewShard()}} 67 | cluster1 := &Cluster{Name: "cluster1", Shards: Shards{NewShard()}} 68 | cluster0.Version.Store(2) 69 | cluster1.Version.Store(3) 70 | 71 | require.NoError(t, store.CreateCluster(ctx, ns, cluster0)) 72 | require.ErrorIs(t, store.CreateCluster(ctx, ns, cluster0), consts.ErrAlreadyExists) 73 | require.NoError(t, store.CreateCluster(ctx, ns, cluster1)) 74 | 75 | gotCluster, err := store.GetCluster(ctx, ns, "cluster0") 76 | require.NoError(t, err) 77 | require.Equal(t, cluster0.Name, gotCluster.Name) 78 | require.Equal(t, cluster0.Version.Load(), gotCluster.Version.Load()) 79 | 80 | gotClusters, err := store.ListCluster(ctx, ns) 81 | require.NoError(t, err) 82 | require.ElementsMatch(t, []string{"cluster0", "cluster1"}, gotClusters) 83 | 84 | require.NoError(t, store.UpdateCluster(ctx, ns, cluster0)) 85 | gotCluster, err = store.GetCluster(ctx, ns, "cluster0") 86 | require.NoError(t, err) 87 | require.Equal(t, cluster0.Name, gotCluster.Name) 88 | require.EqualValues(t, 3, gotCluster.Version.Load()) 89 | 90 | for _, name := range []string{"cluster0", "cluster1"} { 91 | require.NoError(t, store.RemoveCluster(ctx, ns, name)) 92 | _, err = store.GetCluster(ctx, ns, name) 93 | require.ErrorIs(t, err, consts.ErrNotFound) 94 | } 95 | }) 96 | 97 | t.Run("check nodes", func(t *testing.T) { 98 | testCluster, err := NewCluster("test-cluster-another", 99 | []string{"127.0.0.1:1111", "127.0.0.1:2222", "127.0.0.1:3333"}, 1) 100 | require.NoError(t, err) 101 | 102 | require.NoError(t, store.CreateCluster(ctx, "test-ns", testCluster)) 103 | require.NoError(t, store.CheckNewNodes(ctx, []string{"127.0.0.1:4444", "127.0.0.1:5555"})) 104 | require.NotNil(t, store.CheckNewNodes(ctx, []string{"127.0.0.1:3333", "127.0.0.1:4444"})) 105 | require.NotNil(t, store.CheckNewNodes(ctx, []string{"127.0.0.1:2222", "127.0.0.1:3333"})) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /util/network.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package util 21 | 22 | import ( 23 | "net" 24 | "strconv" 25 | "strings" 26 | ) 27 | 28 | func IsHostPort(s string) bool { 29 | parts := strings.Split(s, ":") 30 | if len(parts) != 2 { 31 | return false 32 | } 33 | 34 | return (IsIP(parts[0]) || IsDomain(parts[0])) && IsPort(parts[1]) 35 | } 36 | 37 | func IsIP(ip string) bool { 38 | return net.ParseIP(ip) != nil 39 | } 40 | 41 | func IsPort(port string) bool { 42 | p, err := strconv.Atoi(port) 43 | if err != nil { 44 | return false 45 | } 46 | return p > 0 && p < 65536 47 | } 48 | 49 | func IsDomain(domain string) bool { 50 | _, err := net.LookupHost(domain) 51 | return err == nil 52 | } 53 | -------------------------------------------------------------------------------- /util/string.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package util 21 | 22 | import ( 23 | "math/rand" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | func RandString(length int) string { 29 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 30 | table := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 31 | builder := strings.Builder{} 32 | for i := 0; i < length; i++ { 33 | builder.WriteByte(table[r.Intn(62)]) 34 | } 35 | return builder.String() 36 | } 37 | 38 | func GenerateNodeID() string { 39 | return RandString(40) 40 | } 41 | 42 | func IsUniqueSlice(list interface{}) bool { 43 | switch items := list.(type) { 44 | case []string: 45 | set := make(map[string]struct{}) 46 | for _, item := range items { 47 | _, ok := set[item] 48 | if ok { 49 | return false 50 | } 51 | set[item] = struct{}{} 52 | } 53 | return true 54 | case []int: 55 | set := make(map[int]struct{}) 56 | for _, item := range items { 57 | _, ok := set[item] 58 | if ok { 59 | return false 60 | } 61 | set[item] = struct{}{} 62 | } 63 | return true 64 | } 65 | 66 | panic("only support string and int") 67 | } 68 | -------------------------------------------------------------------------------- /util/string_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package util 21 | 22 | import ( 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestIsUniqueStrings(t *testing.T) { 29 | // unique case 30 | listStr := []string{"a", "b", "c"} 31 | assert.Equal(t, true, IsUniqueSlice(listStr)) 32 | 33 | listInt := []int{1, 2, 3} 34 | assert.Equal(t, true, IsUniqueSlice(listInt)) 35 | 36 | // non unique case 37 | dupListStr := []string{"a", "a", "c"} 38 | assert.Equal(t, false, IsUniqueSlice(dupListStr)) 39 | 40 | dupListStr = []string{"a", "b", "a"} 41 | assert.Equal(t, false, IsUniqueSlice(dupListStr)) 42 | 43 | dupListInt := []int{1, 1, 2, 2} 44 | assert.Equal(t, false, IsUniqueSlice(dupListInt)) 45 | } 46 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | * 19 | */ 20 | package version 21 | 22 | var Version = "unknown" 23 | -------------------------------------------------------------------------------- /webui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "indent": ["warn", 4] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /webui/.gitignore: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | # dependencies 20 | /node_modules 21 | /.pnp 22 | .pnp.js 23 | .yarn/install-state.gz 24 | 25 | # testing 26 | /coverage 27 | 28 | # next.js 29 | /.next/ 30 | /out/ 31 | 32 | # production 33 | /build 34 | 35 | # misc 36 | .DS_Store 37 | *.pem 38 | 39 | # debug 40 | npm-debug.log* 41 | yarn-debug.log* 42 | yarn-error.log* 43 | 44 | # local env files 45 | .env*.local 46 | 47 | # vercel 48 | .vercel 49 | 50 | # typescript 51 | *.tsbuildinfo 52 | next-env.d.ts 53 | 54 | package-lock.json -------------------------------------------------------------------------------- /webui/.prettierignore: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | node_modules 19 | dist 20 | .next 21 | public 22 | build 23 | _build 24 | package-lock.json 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /webui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "es5", 5 | "tabWidth": 4, 6 | "printWidth": 100, 7 | "plugins": ["prettier-plugin-tailwindcss"] 8 | } 9 | -------------------------------------------------------------------------------- /webui/README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | ## Requirements 21 | 22 | - Nodejs >= 18.17 23 | 24 | ## Getting Started 25 | 26 | First install the deployments: 27 | 28 | ```bash 29 | npm install 30 | ``` 31 | 32 | Run the development server: 33 | 34 | ```bash 35 | npm run dev 36 | ``` 37 | 38 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 39 | 40 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 41 | 42 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 43 | 44 | ## Learn More 45 | 46 | This is a [Next.js](https://nextjs.org/) project. 47 | 48 | To learn more about Next.js, take a look at the following resources: 49 | 50 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 51 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 52 | 53 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 54 | -------------------------------------------------------------------------------- /webui/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import { footerConfigType } from "@/app/lib/definitions"; 21 | 22 | export const footerConfig: footerConfigType = { 23 | links: [ 24 | { 25 | title: "Docs", 26 | items: [ 27 | { 28 | label: "Getting started", 29 | to: "/docs/getting-started", 30 | }, 31 | { 32 | label: "Supported commands", 33 | to: "/docs/supported-commands", 34 | }, 35 | { 36 | label: "How to contribute", 37 | to: "/community/contributing", 38 | }, 39 | ], 40 | }, 41 | { 42 | title: "Community", 43 | items: [ 44 | { 45 | label: "Zulip", 46 | href: "https://kvrocks.zulipchat.com/", 47 | }, 48 | { 49 | label: "Issue Tracker", 50 | href: "https://github.com/apache/kvrocks-controller/issues", 51 | }, 52 | { 53 | label: "Mailing list", 54 | href: "https://lists.apache.org/list.html?dev@kvrocks.apache.org", 55 | }, 56 | ], 57 | }, 58 | { 59 | title: "Repositories", 60 | items: [ 61 | { 62 | label: "Kvrocks", 63 | href: "https://github.com/apache/kvrocks", 64 | }, 65 | { 66 | label: "Website", 67 | href: "https://github.com/apache/kvrocks-website", 68 | }, 69 | { 70 | label: "Controller", 71 | href: "https://github.com/apache/kvrocks-controller", 72 | }, 73 | ], 74 | }, 75 | ], 76 | logo: { 77 | height: 128, 78 | width: 320, 79 | alt: "Apache logo", 80 | src: "/asf_logo.svg", 81 | href: "https://www.apache.org/", 82 | }, 83 | copyright: `The Apache Software Foundation. Apache Kvrocks, Kvrocks, and its feather logo are trademarks of The Apache Software Foundation. Redis and its cube logo are registered trademarks of Redis Ltd.`, 84 | }; 85 | -------------------------------------------------------------------------------- /webui/next.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import { PHASE_DEVELOPMENT_SERVER } from "next/constants.js"; 21 | 22 | const apiPrefix = "/api/v1"; 23 | const devHost = "127.0.0.1:9379"; 24 | const prodHost = "production-api.yourdomain.com"; 25 | 26 | const nextConfig = (phase, { defaultConfig }) => { 27 | const isDev = phase === PHASE_DEVELOPMENT_SERVER; 28 | const host = isDev ? devHost : prodHost; 29 | 30 | return { 31 | async rewrites() { 32 | return [ 33 | { 34 | source: `${apiPrefix}/:slug*`, 35 | destination: `http://${host}${apiPrefix}/:slug*`, 36 | }, 37 | ]; 38 | }, 39 | }; 40 | }; 41 | 42 | export default nextConfig; 43 | -------------------------------------------------------------------------------- /webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kvrocks-controller-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.11.3", 13 | "@emotion/styled": "^11.11.0", 14 | "@fortawesome/fontawesome-svg-core": "^6.6.0", 15 | "@fortawesome/free-brands-svg-icons": "^6.6.0", 16 | "@fortawesome/free-solid-svg-icons": "^6.6.0", 17 | "@fortawesome/react-fontawesome": "^0.2.2", 18 | "@mui/icons-material": "^5.15.7", 19 | "@mui/material": "^5.15.5", 20 | "@types/js-yaml": "^4.0.9", 21 | "axios": "^1.6.7", 22 | "js-yaml": "^4.1.0", 23 | "next": "14.1.0", 24 | "react": "^18", 25 | "react-dom": "^18" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^20", 29 | "@types/react": "^18", 30 | "@types/react-dom": "^18", 31 | "autoprefixer": "^10.0.1", 32 | "eslint": "^8", 33 | "eslint-config-next": "14.1.0", 34 | "eslint-config-prettier": "^10.1.1", 35 | "postcss": "^8", 36 | "prettier": "^3.5.3", 37 | "prettier-plugin-tailwindcss": "^0.6.11", 38 | "tailwindcss": "^3.3.0", 39 | "typescript": "^5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webui/postcss.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | module.exports = { 21 | plugins: { 22 | tailwindcss: {}, 23 | autoprefixer: {}, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /webui/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kvrocks-controller/cfc57e27bfb5b5fc7383bad9ae478786411dede0/webui/src/app/favicon.ico -------------------------------------------------------------------------------- /webui/src/app/globals.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @tailwind base; 21 | @tailwind components; 22 | @tailwind utilities; 23 | 24 | @layer components { 25 | .card { 26 | @apply flex flex-col rounded-lg border border-light-border bg-white p-5 shadow-card transition-all hover:shadow-card-hover dark:border-dark-border dark:bg-dark-paper; 27 | } 28 | 29 | .sidebar-item { 30 | @apply my-1 flex items-center rounded-lg px-4 py-2 text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-paper; 31 | } 32 | 33 | .sidebar-item-active { 34 | @apply bg-primary-light/10 text-primary dark:text-primary-light; 35 | } 36 | 37 | .btn { 38 | @apply rounded-md px-4 py-2 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2; 39 | } 40 | 41 | .btn-primary { 42 | @apply bg-primary text-white hover:bg-primary-dark focus:ring-primary; 43 | } 44 | 45 | .btn-outline { 46 | @apply border border-primary text-primary hover:bg-primary hover:text-white focus:ring-primary; 47 | } 48 | 49 | .container-inner { 50 | @apply mx-auto max-w-screen-2xl p-6; 51 | } 52 | } 53 | 54 | /* custom scrollbar */ 55 | ::-webkit-scrollbar { 56 | width: 8px; 57 | height: 8px; 58 | } 59 | 60 | ::-webkit-scrollbar-track { 61 | background: transparent; 62 | } 63 | 64 | ::-webkit-scrollbar-thumb { 65 | background: #c1c1c1; 66 | border-radius: 4px; 67 | } 68 | 69 | ::-webkit-scrollbar-thumb:hover { 70 | background: #a1a1a1; 71 | } 72 | 73 | .dark ::-webkit-scrollbar-thumb { 74 | background: #555; 75 | } 76 | 77 | .dark ::-webkit-scrollbar-thumb:hover { 78 | background: #777; 79 | } 80 | 81 | /* smooth transitions for dark mode */ 82 | body { 83 | transition: background-color 0.3s ease; 84 | } 85 | 86 | .dark body { 87 | background-color: #121212; 88 | color: #e0e0e0; 89 | } 90 | 91 | .MuiAppBar-root { 92 | transition: 93 | background-color 0.3s ease, 94 | color 0.3s ease !important; 95 | } 96 | 97 | .dark .MuiAppBar-root { 98 | background-color: var(--primary-dark, #1565c0) !important; 99 | opacity: 0.9; 100 | } 101 | 102 | .dark .MuiAppBar-root, 103 | .navbar-dark-mode { 104 | background-color: #1565c0 !important; 105 | color: white !important; 106 | opacity: 0.9; 107 | } 108 | 109 | .navbar-dark-mode { 110 | background-color: #1565c0 !important; 111 | color: white !important; 112 | } 113 | 114 | .dark .MuiAppBar-root *, 115 | .navbar-dark-mode * { 116 | color: white !important; 117 | } 118 | 119 | .MuiAppBar-root[class*="navbar-dark-mode"] { 120 | background-color: #1565c0 !important; 121 | } 122 | -------------------------------------------------------------------------------- /webui/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import type { Metadata } from "next"; 21 | import { Inter } from "next/font/google"; 22 | import "./globals.css"; 23 | import Banner from "./ui/banner"; 24 | import { Container } from "@mui/material"; 25 | import { ThemeProvider } from "./theme-provider"; 26 | import Footer from "./ui/footer"; 27 | 28 | const inter = Inter({ subsets: ["latin"] }); 29 | 30 | export const metadata: Metadata = { 31 | title: "Apache Kvrocks Controller", 32 | description: "Management UI for Apache Kvrocks clusters", 33 | }; 34 | 35 | export default function RootLayout({ 36 | children, 37 | }: Readonly<{ 38 | children: React.ReactNode; 39 | }>) { 40 | return ( 41 | 42 | 43 | 44 | 45 | 50 | {children} 51 |