├── .github └── workflows │ ├── e2e-test-ci.yml │ ├── lint.yml │ └── unit-test-ci.yml ├── .gitignore ├── Dockerfile ├── Dockerfile.local ├── LICENSE ├── Makefile ├── README.md ├── ci ├── apisix-seed │ └── conf.yaml ├── apisix │ └── config.yaml ├── docker-compose-arm64.yml └── docker-compose.yml ├── conf └── conf.yaml ├── docs ├── assets │ └── images │ │ └── apisix-seed-overview.png ├── en │ └── latest │ │ ├── nacos.md │ │ └── zookeeper.md └── zh │ └── latest │ ├── nacos.md │ └── zookeeper.md ├── go.mod ├── go.sum ├── internal ├── conf │ ├── conf.go │ ├── nacos.go │ ├── nacos_test.go │ ├── zookeeper.go │ └── zookeeper_test.go ├── core │ ├── components │ │ ├── rewriter.go │ │ ├── rewriter_test.go │ │ ├── watcher.go │ │ └── watcher_test.go │ ├── message │ │ ├── a6conf.go │ │ ├── a6conf_test.go │ │ ├── message.go │ │ └── message_test.go │ └── storer │ │ ├── etcd.go │ │ ├── etcd_test.go │ │ ├── store.go │ │ ├── store_mock.go │ │ ├── store_test.go │ │ └── storehub.go ├── discoverer │ ├── discoverer.go │ ├── discoverer_mock.go │ ├── discovererhub.go │ ├── nacos.go │ ├── nacos_test.go │ ├── zookeeper.go │ └── zookeeper_test.go └── utils │ ├── validate.go │ └── validate_test.go ├── main.go └── test ├── e2e ├── go.mod ├── go.sum ├── regcenter │ ├── regcenter_suite_test.go │ └── regcenter_test.go └── tools │ ├── common │ └── common.go │ ├── regcenter.go │ ├── regcenter │ ├── nacos.go │ └── zookeeper.go │ ├── routes.go │ └── servers.go └── testdata ├── nacos_conf ├── default_value.yaml ├── discoverer.yaml ├── empty.yaml ├── empty_host.yaml ├── host.yaml ├── minimum.yaml ├── pattern.yaml └── set_value.yaml └── validate_test.json /.github/workflows/e2e-test-ci.yml: -------------------------------------------------------------------------------- 1 | name: APISXI-Seed E2E Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run-test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v2 17 | 18 | - name: setup go 19 | uses: actions/setup-go@v2.1.5 20 | with: 21 | go-version: "1.17" 22 | 23 | - name: startup apisix, nacos and apisix-seed 24 | run: | 25 | docker-compose -f ci/docker-compose.yml up -d 26 | sleep 5 27 | docker logs apisix-seed 28 | 29 | - name: install ginkgo cli 30 | run: go install github.com/onsi/ginkgo/v2/ginkgo@v2.0.0 31 | 32 | - name: run tests 33 | working-directory: ./test/e2e 34 | run: ginkgo -r 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | golang-lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: setup go 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: '1.16' 20 | 21 | - name: Download golangci-lint 22 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.41.0 23 | 24 | - name: golangci-lint 25 | run: | 26 | export PATH=$PATH:$(go env GOPATH)/bin/ 27 | make lint 28 | 29 | - name: run gofmt 30 | working-directory: ./ 31 | run: | 32 | diffs=`gofmt -l .` 33 | if [[ -n $diffs ]]; then 34 | echo "Files are not formatted by gofmt:" 35 | echo $diffs 36 | exit 1 37 | fi 38 | 39 | - name: run goimports 40 | working-directory: ./ 41 | run: | 42 | go get golang.org/x/tools/cmd/goimports 43 | export PATH=$PATH:$(go env GOPATH)/bin/ 44 | diffs=`goimports -d .` 45 | if [[ -n $diffs ]]; then 46 | echo "Files are not formatted by goimport:" 47 | echo $diffs 48 | exit 1 49 | fi 50 | -------------------------------------------------------------------------------- /.github/workflows/unit-test-ci.yml: -------------------------------------------------------------------------------- 1 | name: unit-test-ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | jobs: 11 | run-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: setup go 16 | uses: actions/setup-go@v1 17 | with: 18 | go-version: '1.16' 19 | 20 | - name: startup service 21 | run: | 22 | docker-compose -f ci/docker-compose.yml up -d 23 | sleep 5 24 | 25 | - name: run unit test 26 | run: | 27 | make test 28 | - uses: codecov/codecov-action@v2 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | /apisix-seed 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | coverage.txt 22 | conf/conf-*.yaml 23 | log/*.log 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as pre-build 2 | 3 | ARG APISIX_SEED_VERSION=main 4 | 5 | RUN set -x \ 6 | && apk add --no-cache --virtual .builddeps git \ 7 | && git clone https://github.com/api7/apisix-seed.git -b ${APISIX_SEED_VERSION} /usr/local/apisix-seed \ 8 | && cd /usr/local/apisix-seed && git clean -Xdf \ 9 | && rm -f ./.githash && git log --pretty=format:"%h" -1 > ./.githash 10 | 11 | FROM golang:1.17 12 | 13 | COPY --from=pre-build /usr/local/apisix-seed /tmp/apisix-seed/ 14 | 15 | RUN if [ "$ENABLE_PROXY" = "true" ] ; then go env -w GOPROXY=https://goproxy.io,direct ; fi \ 16 | && go env -w GO111MODULE=on \ 17 | && cd /tmp/apisix-seed/ \ 18 | && make build \ 19 | && make install 20 | 21 | ENV PATH=$PATH:/usr/local/apisix-seed 22 | 23 | ENV APISIX_SEED_WORKDIR /usr/local/apisix-seed 24 | 25 | CMD [ "/usr/local/apisix-seed/apisix-seed" ] 26 | 27 | -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | 3 | COPY ./ ./apisix-seed 4 | 5 | RUN if [ "$ENABLE_PROXY" = "true" ] ; then go env -w GOPROXY=https://goproxy.io,direct ; fi \ 6 | && go env -w GO111MODULE=on \ 7 | && cd apisix-seed \ 8 | && make build \ 9 | && make install 10 | 11 | ENV PATH=$PATH:/usr/local/apisix-seed 12 | 13 | ENV APISIX_SEED_WORKDIR /usr/local/apisix-seed 14 | 15 | CMD [ "/usr/local/apisix-seed/apisix-seed" ] 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ENV_INSTALL ?= install 2 | ENV_RM ?= rm -vf 3 | ENV_INST_PREFIX ?= /usr/local 4 | 5 | default: help 6 | 7 | ### help: Show Makefile rules 8 | .PHONY: help 9 | help: 10 | @echo Makefile rules: 11 | @echo 12 | @grep -E '^### [-A-Za-z0-9_]+:' Makefile | sed 's/###/ /' 13 | 14 | ### go-lint: Lint Go source codes 15 | .PHONY: lint 16 | lint: 17 | golangci-lint run --verbose ./... 18 | 19 | ### test: Run the tests of apisix-seed 20 | .PHONY: test 21 | test: 22 | ENV=test go test -race -cover -coverprofile=coverage.txt ./... 23 | 24 | ### build: Build apisix-seed 25 | .PHONY: build 26 | build: 27 | go build 28 | 29 | ### install: Install apisix-seed 30 | .PHONY: install 31 | install: 32 | $(ENV_INSTALL) -d $(ENV_INST_PREFIX)/apisix-seed 33 | $(ENV_INSTALL) -d $(ENV_INST_PREFIX)/apisix-seed/log 34 | $(ENV_INSTALL) -d $(ENV_INST_PREFIX)/apisix-seed/conf 35 | $(ENV_INSTALL) apisix-seed $(ENV_INST_PREFIX)/apisix-seed/ 36 | $(ENV_INSTALL) conf/conf.yaml $(ENV_INST_PREFIX)/apisix-seed/conf/ 37 | 38 | ### uninstall: Uninstall apisix-seed 39 | .PHONY: uninstall 40 | uninstall: 41 | $(ENV_RM) -r $(ENV_INST_PREFIX)/apisix-seed 42 | 43 | ### clean: Clean apisix-seed 44 | .PHONY: clean 45 | clean: 46 | rm -f apisix-seed 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APISIX-Seed for Apache APISIX 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/api7/apisix-seed)](https://goreportcard.com/report/github.com/api7/apisix-seed) 3 | [![Build Status](https://github.com/api7/apisix-seed/workflows/unit-test-ci/badge.svg?branch=main)](https://github.com/api7/apisix-seed/actions) 4 | [![Codecov](https://codecov.io/gh/api7/apisix-seed/branch/main/graph/badge.svg)](https://codecov.io/gh/api7/apisix-seed) 5 | 6 | Do service discovery for Apache APISIX on the Control Plane. 7 | 8 | # What's APISIX-Seed 9 | [Apache APISIX](https://github.com/apache/apisix) is a dynamic, real-time, high-performance API gateway. 10 | 11 | In terms of architecture design, Apache APISIX is divided into two parts: data plane and control plane. The data plane is Apache APISIX itself, which is the component of the traffic proxy and offers many full-featured plugins covering areas such as authentication, security, traffic control, serverless, analytics & monitoring, transformation and logging. 12 | The control plane is mainly used to manage routing, and implement the configuration center through etcd. 13 | 14 | For cloud-native gateways, it is necessary to dynamically obtain the latest service instance information (service discovery) through the service registry. Currently, Apache APISIX already supports [service discovery](https://github.com/apache/apisix/blob/master/docs/en/latest/discovery.md) in the data plane. 15 | 16 | This project is a component of Apache APISIX to implement service discovery in the control plane. It supports cluster deployment. At present, we have supported zookeeper and nacos. We will also support more service registries. 17 | 18 | The following figure is the topology diagram of APISIX-Seed deployment. 19 | 20 | ![apisix-seed overview](./docs/assets/images/apisix-seed-overview.png) 21 | 22 | # Why APISIX-Seed 23 | - Network topology becomes simpler 24 | 25 | > APISIX does not need to maintain a network connection with each registry, and only needs to pay attention to the configuration information in etcd. This will greatly simplify the network topology. 26 | 27 | - Total data volume about upstream service becomes smaller 28 | > Due to the characteristics of the registry, APISIX may store the full amount of registry service data in the worker, such as consul_kv. By introducing APISIX-Seed, each process of APISIX will not need to additionally cache upstream service-related information. 29 | 30 | - Easier to manage 31 | > Service discovery configuration needs to be configured once per APISIX instance. By introducing APISIX-Seed, Apache APISIX will be indifferent to the configuration changes of the service registry. 32 | 33 | # How it works 34 | APISIX-Seed completes data exchange by observing changes in etcd and service registry at the same time. 35 | 36 | As shown in the above architecture diagram, the workflow of APISIX-Seed is as follows: 37 | 38 | 1. Register an upstream with APISIX and specify the service discovery type. APISIX-Seed will watch APISIX resource changes in etcd, filter discovery types, and obtain service names. 39 | 40 | 2. APISIX-Seed subscribes the specified service name to the service registry to obtain changes to the corresponding service. 41 | 42 | 3. After registering the service with the service registry, APISIX-Seed will obtain the new service information and write the updated service node into etcd. 43 | 44 | 4. When the corresponding resources in etcd change, APISIX worker will refresh the latest service node information to memory. 45 | 46 | **It should be noted that after the introduction of APISIX-Seed, if the service of the registry changes frequently, the data in etcd will also change frequently.** 47 | 48 | **The [multi-version concurrency control](https://etcd.io/docs/v3.5/learning/api/#revisions) data model in etcd keeps an exact history of the keyspace.** 49 | 50 | **So, it is best to set the `--auto-compaction` option when starting etcd to compress the history periodically to avoid etcd eventually eventually exhaust its storage space.** 51 | 52 | # Supported service registry 53 | 54 | - [Nacos](docs/en/latest/nacos.md) 55 | - [Zookeeper](docs/en/latest/zookeeper.md) 56 | -------------------------------------------------------------------------------- /ci/apisix-seed/conf.yaml: -------------------------------------------------------------------------------- 1 | etcd: 2 | host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster. 3 | - "http://172.50.238.20:2379" # multiple etcd address, if your etcd cluster enables TLS, please use https scheme, 4 | # e.g. https://127.0.0.1:2379. 5 | prefix: /apisix # apisix configurations prefix 6 | timeout: 30 # 30 seconds 7 | #user: root # root username for etcd 8 | #password: 5tHkHhYkjr6cQY # root password for etcd 9 | tls: 10 | #cert: /path/to/cert # path of certificate used by the etcd client 11 | #key: /path/to/key # path of key used by the etcd client 12 | 13 | verify: true # whether to verify the etcd endpoint certificate when setup a TLS connection to etcd, 14 | # the default value is true, e.g. the certificate will be verified strictly. 15 | log: 16 | level: warn 17 | path: apisix-seed.log 18 | maxage: 24h 19 | maxsize: 102400 20 | rotation_time: 1h 21 | 22 | discovery: # service discovery center 23 | nacos: 24 | host: # it's possible to define multiple nacos hosts addresses of the same nacos cluster. 25 | - "http://172.50.238.30:8848" 26 | prefix: /nacos 27 | weight: 100 # default weight for node 28 | timeout: 29 | connect: 2000 # default 2000ms 30 | send: 2000 # default 2000ms 31 | read: 5000 # default 5000ms 32 | zookeeper: 33 | hosts: 34 | - "172.50.238.40:2181" 35 | prefix: /zookeeper 36 | weight: 100 # default weight for node 37 | timeout: 10 # default 10s 38 | -------------------------------------------------------------------------------- /ci/apisix/config.yaml: -------------------------------------------------------------------------------- 1 | deployment: 2 | etcd: 3 | host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster. 4 | - "http://172.50.238.20:2379" # multiple etcd address, if your etcd cluster enables TLS, please use https scheme, 5 | prefix: /apisix # apisix configurations prefix 6 | timeout: 30 # 30 seconds 7 | #resync_delay: 5 # when sync failed and a rest is needed, resync after the configured seconds plus 50% random jitter 8 | #health_check_timeout: 10 # etcd retry the unhealthy nodes after the configured seconds 9 | startup_retry: 2 # etcd retry time that only affects the health check, default 2 10 | 11 | admin: 12 | allow_admin: # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow 13 | - 0.0.0.0/0 14 | 15 | -------------------------------------------------------------------------------- /ci/docker-compose-arm64.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | apisix: 5 | image: apache/apisix:2.12.1-alpine 6 | restart: always 7 | privileged: true 8 | volumes: 9 | - ./apisix/config.yaml:/usr/local/apisix/conf/config.yaml:ro 10 | depends_on: 11 | - etcd 12 | ports: 13 | - "9180:9180/tcp" 14 | - "9080:9080/tcp" 15 | - "9443:9443/tcp" 16 | networks: 17 | apisix-seed: 18 | ipv4_address: 172.50.238.10 19 | 20 | etcd: 21 | image: rancher/coreos-etcd:v3.4.15-arm64 22 | user: root 23 | restart: always 24 | environment: 25 | ETCD_UNSUPPORTED_ARCH: "arm64" 26 | ETCD_ENABLE_V2: "true" 27 | ALLOW_NONE_AUTHENTICATION: "yes" 28 | ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379" 29 | ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" 30 | ports: 31 | - "2379:2379/tcp" 32 | networks: 33 | apisix-seed: 34 | ipv4_address: 172.50.238.20 35 | 36 | nacos_no_auth: 37 | image: hongtu1993/nacos:2.0.4 38 | environment: 39 | - MODE=standalone 40 | - JVM_XMS=512m 41 | - JVM_XMX=512m 42 | restart: unless-stopped 43 | ports: 44 | - "8848:8848/tcp" 45 | networks: 46 | apisix-seed: 47 | ipv4_address: 172.50.238.30 48 | 49 | ## Zookeeper 50 | zookeeper: 51 | image: zookeeper:3.7.0 52 | restart: unless-stopped 53 | ports: 54 | - "2181:2181/tcp" 55 | networks: 56 | apisix-seed: 57 | ipv4_address: 172.50.238.40 58 | 59 | apisix_seed_dev: 60 | build: 61 | context: ./.. 62 | dockerfile: Dockerfile.local 63 | restart: always 64 | volumes: 65 | - ./apisix-seed/conf.yaml:/usr/local/apisix-seed/conf/conf.yaml:ro 66 | depends_on: 67 | - etcd 68 | - nacos_no_auth 69 | networks: 70 | apisix-seed: 71 | ipv4_address: 172.50.238.50 72 | 73 | networks: 74 | apisix-seed: 75 | driver: bridge 76 | ipam: 77 | driver: default 78 | config: 79 | - subnet: 172.50.238.0/24 80 | gateway: 172.50.238.1 81 | 82 | -------------------------------------------------------------------------------- /ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | apisix: 5 | image: apache/apisix:dev 6 | restart: always 7 | volumes: 8 | - ./apisix/config.yaml:/usr/local/apisix/conf/config.yaml:ro 9 | depends_on: 10 | - etcd 11 | ports: 12 | - "9180:9180/tcp" 13 | - "9080:9080/tcp" 14 | - "9443:9443/tcp" 15 | networks: 16 | apisix-seed: 17 | ipv4_address: 172.50.238.10 18 | 19 | etcd: 20 | image: bitnami/etcd:3.4.9 21 | restart: always 22 | environment: 23 | ETCD_ENABLE_V2: "true" 24 | ALLOW_NONE_AUTHENTICATION: "yes" 25 | ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379" 26 | ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" 27 | ports: 28 | - "2379:2379/tcp" 29 | networks: 30 | apisix-seed: 31 | ipv4_address: 172.50.238.20 32 | 33 | nacos_no_auth: 34 | image: nacos/nacos-server:1.4.1 35 | environment: 36 | - MODE=standalone 37 | - JVM_XMS=512m 38 | - JVM_XMX=512m 39 | restart: unless-stopped 40 | ports: 41 | - "8848:8848/tcp" 42 | networks: 43 | apisix-seed: 44 | ipv4_address: 172.50.238.30 45 | 46 | ## Zookeeper 47 | zookeeper: 48 | image: zookeeper:3.7.0 49 | restart: unless-stopped 50 | ports: 51 | - "2181:2181/tcp" 52 | networks: 53 | apisix-seed: 54 | ipv4_address: 172.50.238.40 55 | 56 | apisix_seed_dev: 57 | build: 58 | context: ./.. 59 | dockerfile: Dockerfile.local 60 | restart: always 61 | container_name: apisix-seed 62 | volumes: 63 | - ./apisix-seed/conf.yaml:/usr/local/apisix-seed/conf/conf.yaml:ro 64 | depends_on: 65 | - etcd 66 | - nacos_no_auth 67 | networks: 68 | apisix-seed: 69 | ipv4_address: 172.50.238.50 70 | 71 | networks: 72 | apisix-seed: 73 | driver: bridge 74 | ipam: 75 | driver: default 76 | config: 77 | - subnet: 172.50.238.0/24 78 | gateway: 172.50.238.1 79 | 80 | -------------------------------------------------------------------------------- /conf/conf.yaml: -------------------------------------------------------------------------------- 1 | etcd: 2 | host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster. 3 | - "http://127.0.0.1:2379" # multiple etcd address, if your etcd cluster enables TLS, please use https scheme, 4 | prefix: /apisix # apisix configurations prefix 5 | timeout: 30 # 30 seconds 6 | #user: root # root username for etcd 7 | #password: 5tHkHhYkjr6cQY # root password for etcd 8 | tls: 9 | #cert: /path/to/cert # path of certificate used by the etcd client 10 | #key: /path/to/key # path of key used by the etcd client 11 | 12 | verify: true # whether to verify the etcd endpoint certificate when setup a TLS connection to etcd, 13 | # the default value is true, e.g. the certificate will be verified strictly. 14 | log: 15 | level: warn 16 | path: apisix-seed.log # path is the file to write logs to. Backup log files will be retained in the same directory 17 | maxage: 168h # maxage is the maximum number of days to retain old log files based on the timestamp encoded in their filename 18 | maxsize: 104857600 # maxsize is the maximum size in megabytes of the log file before it gets rotated. It defaults to 100mb 19 | rotation_time: 1h # rotation_time is the log rotation time 20 | 21 | discovery: # service discovery center 22 | nacos: 23 | host: # it's possible to define multiple nacos hosts addresses of the same nacos cluster. 24 | - "http://127.0.0.1:8848" 25 | prefix: /nacos 26 | user: "admin" # username for nacos 27 | password: "5tHkHhYkjr6cQY" # password for nacos 28 | weight: 100 # default weight for node 29 | timeout: 30 | connect: 2000 # default 2000ms 31 | send: 2000 # default 2000ms 32 | read: 5000 # default 5000ms 33 | zookeeper: 34 | hosts: 35 | - "127.0.0.1:2181" 36 | prefix: /zookeeper 37 | weight: 100 # default weight for node 38 | timeout: 10 # default 10s 39 | -------------------------------------------------------------------------------- /docs/assets/images/apisix-seed-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api7/apisix-seed/bd8fb778b32a3c18a01685fb88729935307ef54f/docs/assets/images/apisix-seed-overview.png -------------------------------------------------------------------------------- /docs/en/latest/nacos.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: nacos 3 | keywords: 4 | - APISIX 5 | - Nacos 6 | - apisix-seed 7 | description: This document contains information about how to use Nacos as service registry in Apache APISIX via apisix-seed. 8 | --- 9 | 10 | 28 | 29 | # Installation 30 | 31 | ## Deploy Nacos 32 | 33 | Quickly deploy Nacos using the Nacos Docker image: 34 | ```bash 35 | docker run --name nacos-quick -e MODE=standalone -p 8848:8848 -d nacos/nacos-server:1.4.1 36 | ``` 37 | 38 | ## Install APISIX-Seed 39 | 40 | Download and build APISIX-Seed: 41 | ```bash 42 | git clone https://github.com/api7/apisix-seed.git 43 | cd apisix-seed 44 | make build && make install 45 | ``` 46 | 47 | The default configuration file is in `/usr/local/apisix-seed/conf/conf.yaml` with the following contents: 48 | ```yaml 49 | etcd: # APISIX etcd Configure 50 | host: 51 | - "http://127.0.0.1:2379" 52 | prefix: /apisix 53 | timeout: 30 54 | 55 | discovery: # service discovery center 56 | nacos: 57 | host: # it's possible to define multiple nacos hosts addresses of the same nacos cluster. 58 | - "http://127.0.0.1:8848" 59 | prefix: /nacos 60 | weight: 100 # default weight for node 61 | timeout: 62 | connect: 2000 # default 2000ms 63 | send: 2000 # default 2000ms 64 | read: 5000 # default 5000ms 65 | ``` 66 | You can easily understand each configuration item, we will not explain it additionally. 67 | 68 | Start APISIX-Seed: 69 | ```bash 70 | APISIX_SEED_WORKDIR=/usr/local/apisix-seed /usr/local/apisix-seed/apisix-seed 71 | ``` 72 | 73 | ## Register the upstream service 74 | 75 | Start the httpbin service via Docker: 76 | ```bash 77 | docker run -d -p 8080:80 --rm kennethreitz/httpbin 78 | ``` 79 | 80 | Register the service to Nacos: 81 | ```bash 82 | curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=httpbin&ip=127.0.0.1&port=8080' 83 | ``` 84 | 85 | ## Verify in Apache APISIX 86 | 87 | Start Apache APISIX with default configuration: 88 | ```bash 89 | apisix start 90 | ``` 91 | 92 | Create a Route through the Admin API interface of Apache APISIX: 93 | ```bash 94 | curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' 95 | { 96 | "uris": "/*", 97 | "hosts": [ 98 | "httpbin" 99 | ], 100 | "upstream": { 101 | "discovery_type": "nacos", 102 | "service_name": "httpbin", 103 | "type": "roundrobin" 104 | } 105 | }' 106 | ``` 107 | 108 | Send a request to confirm whether service discovery is in effect: 109 | ```bash 110 | curl http://127.0.0.1:9080/get -H 'Host: httpbin' 111 | ``` 112 | 113 | # Features 114 | 115 | ## Metadata 116 | 117 | APISIX-SEED supports the grouping of services according to metadata, let's look at an example 118 | 119 | Register 2 instances within nacos: 120 | 121 | ``` 122 | curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=httpbin&ip=127.0.0.1&port=8080&metadata=%7B%22version%22:%22v1%22%7D&ephemeral=false' 123 | 124 | curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=httpbin&ip=127.0.0.1&port=8081&metadata=%7B%22version%22:%22v2%22%7D&ephemeral=false' 125 | 126 | ``` 127 | 128 | Where the metadata information for instance 127.0.0.1:8080 is {"version": "v1"} 129 | 130 | And the metadata information for instance 127.0.0.1:8081 is {"version": "v2"} 131 | 132 | Configure APISIX to only fetch upstream instances with version = v1 133 | 134 | ``` 135 | curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' 136 | { 137 | "uri": "/*", 138 | "hosts": [ 139 | "httpbin" 140 | ], 141 | "upstream": { 142 | "discovery_type": "nacos", 143 | "service_name": "httpbin", 144 | "type": "roundrobin", 145 | "discovery_args": { 146 | "metadata": { 147 | "version": "v1" 148 | } 149 | } 150 | } 151 | }' 152 | ``` 153 | 154 | Check the upstream 155 | 156 | ``` 157 | curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' 158 | 159 | { 160 | "value": { 161 | "hosts": [ 162 | "httpbin" 163 | ], 164 | "update_time": 1677482525, 165 | "upstream": { 166 | "hash_on": "vars", 167 | "nodes": [ 168 | { 169 | "weight": 1, 170 | "port": 8080, 171 | "host": "127.0.0.1" 172 | } 173 | ], 174 | "discovery_args": { 175 | "metadata": { 176 | "version": "v1" 177 | } 178 | }, 179 | "_service_name": "httpbin", 180 | "_discovery_type": "nacos", 181 | "pass_host": "pass", 182 | "scheme": "http", 183 | "type": "roundrobin" 184 | }, 185 | "id": "1", 186 | "status": 1, 187 | "create_time": 1677482525, 188 | "uri": "/*", 189 | "priority": 0 190 | }, 191 | "key": "/apisix/routes/1", 192 | "createdIndex": 557, 193 | "modifiedIndex": 559 194 | } 195 | ``` 196 | 197 | As you can see, only the upstream instance 127.0.0.1:8080 with version = v1 is fetched 198 | -------------------------------------------------------------------------------- /docs/en/latest/zookeeper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: zookeeper 3 | keywords: 4 | - APISIX 5 | - ZooKeeper 6 | - apisix-seed 7 | description: This document contains information about how to use zookeeper as service registry in Apache APISIX via apisix-seed. 8 | --- 9 | 10 | 28 | 29 | ## Setting `apisix-seed` and Zookeeper 30 | 31 | The configuration steps are as follows: 32 | 33 | 1. Start the Zookeeper service 34 | 35 | ```bash 36 | docker run -itd --rm --name=dev-zookeeper -p 2181:2181 zookeeper:3.7.0 37 | ``` 38 | 39 | 2. Download and compile the `apisix-seed` project. 40 | 41 | ```bash 42 | git clone https://github.com/api7/apisix-seed.git 43 | cd apisix-seed 44 | go build 45 | ``` 46 | 47 | 3. Modify the `apisix-seed` configuration file, config path `conf/conf.yaml`. 48 | 49 | ```bash 50 | etcd: # APISIX ETCD Configure 51 | host: 52 | - "http://127.0.0.1:2379" 53 | prefix: /apisix 54 | timeout: 30 55 | 56 | discovery: 57 | zookeeper: # Zookeeper Service Discovery 58 | hosts: 59 | - "127.0.0.1:2181" # Zookeeper service address 60 | prefix: /zookeeper 61 | weight: 100 # default weight for node 62 | timeout: 10 # default 10s 63 | ``` 64 | 65 | 4. Start `apisix-seed` to monitor service changes 66 | 67 | ```bash 68 | ./apisix-seed 69 | ``` 70 | 71 | ## Setting `APISIX` Route and Upstream 72 | 73 | Set a route, the request path is `/zk/*`, the upstream uses zookeeper as service discovery, and the service name 74 | is `APISIX-ZK`. 75 | 76 | ```shell 77 | curl http://127.0.0.1:9080/apisix/admin/routes/1 \ 78 | -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' 79 | { 80 | "uri": "/zk/*", 81 | "upstream": { 82 | "service_name": "APISIX-ZK", 83 | "type": "roundrobin", 84 | "discovery_type": "zookeeper" 85 | } 86 | }' 87 | ``` 88 | 89 | ## Register Service and verify Request 90 | 91 | 1. Service registration using Zookeeper CLI 92 | 93 | - Register Service 94 | 95 | ```bash 96 | # Login Container 97 | docker exec -it ${CONTAINERID} /bin/bash 98 | # Login Zookeeper Client 99 | root@ae2f093337c1:/apache-zookeeper-3.7.0-bin# ./bin/zkCli.sh 100 | # Register Service 101 | [zk: localhost:2181(CONNECTED) 0] create /zookeeper/APISIX-ZK '[{"host":"127.0.0.1","port":1980,"weight":100}]' 102 | ``` 103 | 104 | - Successful Response 105 | 106 | ```bash 107 | Created /zookeeper/APISIX-ZK 108 | ``` 109 | 110 | 2. Verify Request 111 | 112 | - Request 113 | 114 | ```bash 115 | curl -i http://127.0.0.1:9080/zk/hello 116 | ``` 117 | 118 | - Response 119 | 120 | ```bash 121 | HTTP/1.1 200 OK 122 | Connection: keep-alive 123 | Content-Type: text/html; charset=utf-8 124 | Date: Tue, 29 Mar 2022 08:51:28 GMT 125 | Server: APISIX/2.12.0 126 | Transfer-Encoding: chunked 127 | 128 | hello 129 | ``` 130 | -------------------------------------------------------------------------------- /docs/zh/latest/nacos.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: nacos 3 | keywords: 4 | - APISIX 5 | - Nacos 6 | - apisix-seed 7 | description: 本篇文档介绍了如何通过 apisix-seed 在 Apache APISIX 中使用 Nacos 做服务发现。 8 | --- 9 | 10 | 28 | 29 | # 安装 30 | ## 部署 Nacos 31 | 32 | 使用 Nacos Docker 镜像快速部署 Nacos: 33 | ```bash 34 | docker run --name nacos-quick -e MODE=standalone -p 8848:8848 -d nacos/nacos-server:1.4.1 35 | ``` 36 | 37 | ## 安装 APISIX-Seed 38 | 39 | 下载并构建 APISIX-Seed: 40 | ```bash 41 | git clone https://github.com/api7/apisix-seed.git 42 | cd apisix-seed 43 | make build && make install 44 | ``` 45 | 46 | 默认配置文件在 `/usr/local/apisix-seed/conf/conf.yaml` 中,内容如下: 47 | ```yaml 48 | etcd: # APISIX etcd Configure 49 | host: 50 | - "http://127.0.0.1:2379" 51 | prefix: /apisix 52 | timeout: 30 53 | 54 | discovery: # service discovery center 55 | nacos: 56 | host: # it's possible to define multiple nacos hosts addresses of the same nacos cluster. 57 | - "http://127.0.0.1:8848" 58 | prefix: /nacos 59 | weight: 100 # default weight for node 60 | timeout: 61 | connect: 2000 # default 2000ms 62 | send: 2000 # default 2000ms 63 | read: 5000 # default 5000ms 64 | ``` 65 | 每个配置项大家可以很容易的理解,我们不再赘述。 66 | 67 | 启动 APISIX-Seed: 68 | ```bash 69 | APISIX_SEED_WORKDIR=/usr/local/apisix-seed /usr/local/apisix-seed/apisix-seed 70 | ``` 71 | 72 | ## 注册上游服务 73 | 74 | 通过 Docker 启动 httpbin 服务: 75 | ```bash 76 | docker run -d -p 8080:80 --rm kennethreitz/httpbin 77 | ``` 78 | 79 | 将服务注册到 Nacos: 80 | ```bash 81 | curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=httpbin&ip=127.0.0.1&port=8080' 82 | ``` 83 | 84 | ## 在 Apache APISIX 中验证 85 | 86 | 使用默认配置启动 Apache APISIX: 87 | ```bash 88 | apisix start 89 | ``` 90 | 91 | 通过 Apache APISIX 的 Admin API 接口创建路由: 92 | ```bash 93 | curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' 94 | { 95 | "uri": "/*", 96 | "hosts": [ 97 | "httpbin" 98 | ], 99 | "upstream": { 100 | "discovery_type": "nacos", 101 | "service_name": "httpbin", 102 | "type": "roundrobin" 103 | } 104 | }' 105 | ``` 106 | 107 | 发送请求确认服务发现是否生效: 108 | ```bash 109 | curl http://127.0.0.1:9080/get -H 'Host: httpbin' 110 | ``` 111 | 112 | # 功能介绍 113 | ## 支持 metadata 114 | 115 | APISIX-SEED 支持根据 metadata 来对服务进行分组,让我们来看一个例子 116 | 117 | 在 nacos 内部注册 2 个实例: 118 | 119 | ``` 120 | curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=httpbin&ip=127.0.0.1&port=8080&metadata=%7B%22version%22:%22v1%22%7D&ephemeral=false' 121 | 122 | curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=httpbin&ip=127.0.0.1&port=8081&metadata=%7B%22version%22:%22v2%22%7D&ephemeral=false' 123 | 124 | ``` 125 | 126 | 其中实例 127.0.0.1:8080 的 metadata 信息为 {"version":"v1"}, 127 | 128 | 实例 127.0.0.1:8081 的 metadata 信息为 {"version":"v2"} 129 | 130 | 在 APISIX 中配置只获取 version = v1 的上游实例 131 | 132 | ``` 133 | curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' 134 | { 135 | "uri": "/*", 136 | "hosts": [ 137 | "httpbin" 138 | ], 139 | "upstream": { 140 | "discovery_type": "nacos", 141 | "service_name": "httpbin", 142 | "type": "roundrobin", 143 | "discovery_args": { 144 | "metadata": { 145 | "version": "v1" 146 | } 147 | } 148 | } 149 | }' 150 | ``` 151 | 152 | 查询 153 | 154 | ``` 155 | curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' 156 | 157 | { 158 | "value": { 159 | "hosts": [ 160 | "httpbin" 161 | ], 162 | "update_time": 1677482525, 163 | "upstream": { 164 | "hash_on": "vars", 165 | "nodes": [ 166 | { 167 | "weight": 1, 168 | "port": 8080, 169 | "host": "127.0.0.1" 170 | } 171 | ], 172 | "discovery_args": { 173 | "metadata": { 174 | "version": "v1" 175 | } 176 | }, 177 | "_service_name": "httpbin", 178 | "_discovery_type": "nacos", 179 | "pass_host": "pass", 180 | "scheme": "http", 181 | "type": "roundrobin" 182 | }, 183 | "id": "1", 184 | "status": 1, 185 | "create_time": 1677482525, 186 | "uri": "/*", 187 | "priority": 0 188 | }, 189 | "key": "/apisix/routes/1", 190 | "createdIndex": 557, 191 | "modifiedIndex": 559 192 | } 193 | ``` 194 | 195 | 可以看到,只获取到了 version = v1 的上游实例 127.0.0.1:8080 196 | -------------------------------------------------------------------------------- /docs/zh/latest/zookeeper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: zookeeper 3 | keywords: 4 | - APISIX 5 | - ZooKeeper 6 | - apisix-seed 7 | description: 本篇文档介绍了如何通过 apisix-seed 在 Apache APISIX 中使用 ZooKeeper 做服务发现。 8 | --- 9 | 10 | 28 | 29 | ## 环境准备:配置 `apisix-seed` 和 ZooKeeper 30 | 31 | 1. 启动 ZooKeeper 32 | 33 | ```bash 34 | docker run -itd --rm --name=dev-zookeeper -p 2181:2181 zookeeper:3.7.0 35 | ``` 36 | 37 | 2. 下载并编译 `apisix-seed` 项目 38 | 39 | ```bash 40 | git clone https://github.com/api7/apisix-seed.git 41 | cd apisix-seed 42 | go build 43 | ``` 44 | 45 | 3. 参考以下信息修改 `apisix-seed` 配置文件,路径为 `conf/conf.yaml` 46 | 47 | ```bash 48 | etcd: # APISIX etcd 配置 49 | host: 50 | - "http://127.0.0.1:2379" 51 | prefix: /apisix 52 | timeout: 30 53 | 54 | discovery: 55 | zookeeper: # 配置 ZooKeeper 进行服务发现 56 | hosts: 57 | - "127.0.0.1:2181" # ZooKeeper 服务器地址 58 | prefix: /zookeeper 59 | weight: 100 # ZooKeeper 节点默认权重设为 100 60 | timeout: 10 # ZooKeeper 会话超时时间默认设为 10 秒 61 | ``` 62 | 63 | 4. 启动 `apisix-seed` 以监听服务变更 64 | 65 | ```bash 66 | ./apisix-seed 67 | ``` 68 | 69 | ## 设置 APISIX 路由和上游 70 | 71 | 通过以下命令设置路由,请求路径设置为 `/zk/*`,上游使用 ZooKeeper 作为服务发现,服务名称为 `APISIX-ZK`。 72 | 73 | ```shell 74 | curl http://127.0.0.1:9080/apisix/admin/routes/1 \ 75 | -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' 76 | { 77 | "uri": "/zk/*", 78 | "upstream": { 79 | "service_name": "APISIX-ZK", 80 | "type": "roundrobin", 81 | "discovery_type": "zookeeper" 82 | } 83 | }' 84 | ``` 85 | 86 | ## 注册服务 87 | 88 | 使用 ZooKeeper-cli 注册服务 89 | 90 | 登录 ZooKeeper 容器,使用 CLI 程序进行服务注册。具体命令如下: 91 | 92 | ```bash 93 | # 登陆容器 94 | docker exec -it ${CONTAINERID} /bin/bash 95 | # 登陆 ZooKeeper 客户端 96 | root@ae2f093337c1:/apache-zookeeper-3.7.0-bin# ./bin/zkCli.sh 97 | # 注册服务 98 | [zk: localhost:2181(CONNECTED) 0] create /zookeeper/APISIX-ZK '[{"host":"127.0.0.1","port":1980,"weight":100}]' 99 | ``` 100 | 101 | 返回结果如下: 102 | 103 | ```bash 104 | Created /zookeeper/APISIX-ZK 105 | ``` 106 | 107 | ## 请求验证 108 | 109 | 通过以下命令请求路由: 110 | 111 | ```bash 112 | curl -i http://127.0.0.1:9080/zk/hello 113 | ``` 114 | 115 | 正常返回结果: 116 | 117 | ```bash 118 | HTTP/1.1 200 OK 119 | Connection: keep-alive 120 | ... 121 | hello 122 | ``` 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/api7/apisix-seed 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/api7/gopkg v0.2.0 7 | github.com/go-zookeeper/zk v1.0.2 8 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible 9 | github.com/nacos-group/nacos-sdk-go v1.1.1 10 | github.com/stretchr/testify v1.7.1 11 | github.com/xeipuuv/gojsonschema v1.2.0 12 | go.etcd.io/etcd/client/pkg/v3 v3.5.6 13 | go.etcd.io/etcd/client/v3 v3.5.6 14 | go.uber.org/zap v1.18.1 15 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 16 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 17 | ) 18 | 19 | require ( 20 | github.com/aliyun/alibaba-cloud-sdk-go v1.61.18 // indirect 21 | github.com/buger/jsonparser v1.1.1 // indirect 22 | github.com/coreos/go-semver v0.3.0 // indirect 23 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/go-errors/errors v1.0.1 // indirect 26 | github.com/gogo/protobuf v1.3.2 // indirect 27 | github.com/golang/protobuf v1.5.2 // indirect 28 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect 29 | github.com/jonboulle/clockwork v0.3.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/lestrrat-go/strftime v1.0.6 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/pkg/errors v0.9.1 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/smartystreets/goconvey v1.6.4 // indirect 37 | github.com/stretchr/objx v0.1.1 // indirect 38 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 39 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 40 | go.etcd.io/etcd/api/v3 v3.5.6 // indirect 41 | go.uber.org/atomic v1.7.0 // indirect 42 | go.uber.org/multierr v1.6.0 // indirect 43 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 44 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect 45 | golang.org/x/text v0.3.6 // indirect 46 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect 47 | google.golang.org/grpc v1.41.0 // indirect 48 | google.golang.org/protobuf v1.28.0 // indirect 49 | gopkg.in/ini.v1 v1.51.0 // indirect 50 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 51 | ) 52 | 53 | replace github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 => github.com/buger/jsonparser v1.1.1 54 | -------------------------------------------------------------------------------- /internal/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type DisBuilder func([]byte) (interface{}, error) 14 | 15 | var ( 16 | WorkDir = "." 17 | ETCDConfig *Etcd 18 | LogConfig *Log 19 | DisConfigs = make(map[string]interface{}) 20 | DisBuilders = make(map[string]DisBuilder) 21 | ) 22 | 23 | type TLS struct { 24 | CertFile string `yaml:"cert"` 25 | KeyFile string `yaml:"key"` 26 | Verify bool 27 | } 28 | 29 | type Etcd struct { 30 | Host []string 31 | Prefix string 32 | Timeout int 33 | User string 34 | Password string 35 | TLS *TLS 36 | } 37 | 38 | type Log struct { 39 | Level string 40 | Path string 41 | MaxAge time.Duration 42 | MaxSize int64 43 | RotationTime time.Duration `yaml:"rotation_time"` 44 | } 45 | 46 | type Config struct { 47 | Etcd Etcd 48 | Log Log 49 | Discovery map[string]interface{} 50 | } 51 | 52 | func InitConf() { 53 | if workDir := os.Getenv("APISIX_SEED_WORKDIR"); workDir != "" { 54 | WorkDir = workDir 55 | } 56 | 57 | filePath := WorkDir + "/conf/conf.yaml" 58 | if configurationContent, err := ioutil.ReadFile(filePath); err != nil { 59 | panic(fmt.Sprintf("fail to read configuration: %s", filePath)) 60 | } else { 61 | config := Config{} 62 | err := yaml.Unmarshal(configurationContent, &config) 63 | if err != nil { 64 | log.Printf("conf: %s, error: %v", configurationContent, err) 65 | } 66 | 67 | for name, rawConfig := range config.Discovery { 68 | builder, ok := DisBuilders[name] 69 | if !ok { 70 | panic(fmt.Sprintf("unkown discovery configuration: %s", name)) 71 | } 72 | 73 | rawStr, _ := yaml.Marshal(rawConfig) 74 | disConfig, err := builder(rawStr) 75 | if err != nil { 76 | panic(fmt.Sprintf("fail to load discovery configuration: %s", err)) 77 | } 78 | 79 | DisConfigs[name] = disConfig 80 | } 81 | 82 | initLogConfig(config.Log) 83 | 84 | if len(config.Etcd.Host) > 0 { 85 | initEtcdConfig(config.Etcd) 86 | } 87 | } 88 | } 89 | 90 | // initialize etcd config 91 | func initEtcdConfig(conf Etcd) { 92 | var host = []string{"127.0.0.1:2379"} 93 | if len(conf.Host) > 0 { 94 | host = conf.Host 95 | } 96 | 97 | prefix := "/apisix" 98 | if len(conf.Prefix) > 0 { 99 | prefix = conf.Prefix 100 | } 101 | 102 | ETCDConfig = &Etcd{ 103 | Host: host, 104 | User: conf.User, 105 | Password: conf.Password, 106 | TLS: conf.TLS, 107 | Prefix: prefix, 108 | } 109 | } 110 | 111 | func initLogConfig(conf Log) { 112 | level := conf.Level 113 | if level == "" { 114 | level = "warn" 115 | } 116 | if conf.Path == "" { 117 | LogConfig = &Log{ 118 | Level: level, 119 | } 120 | return 121 | } 122 | maxAge := conf.MaxAge 123 | if maxAge == 0 { 124 | maxAge = 7 * 24 * time.Hour 125 | } 126 | maxSize := conf.MaxSize 127 | if maxSize == 0 { 128 | maxSize = 100 * 1024 * 1024 129 | } 130 | rotationTime := conf.RotationTime 131 | if rotationTime == 0 { 132 | rotationTime = time.Hour 133 | } 134 | LogConfig = &Log{ 135 | Level: level, 136 | Path: conf.Path, 137 | MaxAge: maxAge, 138 | MaxSize: maxSize, 139 | RotationTime: rotationTime, 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /internal/conf/nacos.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/api7/apisix-seed/internal/utils" 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | func init() { 9 | DisBuilders["nacos"] = nacosBuilder 10 | } 11 | 12 | const schema = ` 13 | { 14 | "type": "object", 15 | "properties": { 16 | "Host": { 17 | "type": "array", 18 | "minItems": 1, 19 | "items": { 20 | "type": "string", 21 | "pattern": "^http(s)?:\\/\\/[a-zA-Z0-9-_.:]+$", 22 | "minLength": 2, 23 | "maxLength": 100 24 | } 25 | }, 26 | "Prefix": { 27 | "type": "string", 28 | "pattern": "^[\\/a-zA-Z0-9-_.]*$", 29 | "maxLength": 100 30 | }, 31 | "Weight": { 32 | "type": "integer", 33 | "minimum": 1, 34 | "default": 100 35 | }, 36 | "User": { 37 | "type": "string" 38 | }, 39 | "Password": { 40 | "type": "string" 41 | }, 42 | "Timeout": { 43 | "type": "object", 44 | "properties": { 45 | "Connect": { 46 | "type": "integer", 47 | "minimum": 1, 48 | "default": 2000 49 | }, 50 | "Send": { 51 | "type": "integer", 52 | "minimum": 1, 53 | "default": 2000 54 | }, 55 | "Read": { 56 | "type": "integer", 57 | "minimum": 1, 58 | "default": 5000 59 | } 60 | } 61 | } 62 | }, 63 | "required": [ 64 | "Host" 65 | ] 66 | } 67 | ` 68 | 69 | type timeout struct { 70 | Connect int 71 | Send int 72 | Read int 73 | } 74 | 75 | type Nacos struct { 76 | Host []string 77 | Prefix string 78 | User string 79 | Password string 80 | Weight int 81 | Timeout timeout 82 | } 83 | 84 | func nacosBuilder(content []byte) (interface{}, error) { 85 | // go jsonschema lib doesn't support setting default values 86 | // so we need to set for some default fields ourselves. 87 | nacos := Nacos{ 88 | Weight: 100, 89 | Timeout: timeout{ 90 | Connect: 2000, 91 | Send: 2000, 92 | Read: 5000, 93 | }, 94 | } 95 | err := yaml.Unmarshal(content, &nacos) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | validator, err := utils.NewJsonSchemaValidator(schema) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | if err = validator.Validate(nacos); err != nil { 106 | return nil, err 107 | } 108 | return &nacos, nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/conf/nacos_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func ReadFile(t *testing.T, file string) []byte { 16 | wd, _ := os.Getwd() 17 | dir := wd[:strings.Index(wd, "internal")] 18 | path := filepath.Join(dir, "test/testdata/nacos_conf/", file) 19 | bs, err := ioutil.ReadFile(path) 20 | assert.Nil(t, err) 21 | return bs 22 | } 23 | 24 | func TestNacosValidator(t *testing.T) { 25 | tests := []struct { 26 | caseDesc string 27 | givePath string 28 | wantValidateErr []error 29 | }{ 30 | { 31 | caseDesc: "Test Required Host: empty content", 32 | givePath: "empty.yaml", 33 | wantValidateErr: []error{ 34 | fmt.Errorf("Host: Invalid type. Expected: array, given: null"), 35 | }, 36 | }, 37 | { 38 | caseDesc: "Test Required Host: empty array", 39 | givePath: "empty_host.yaml", 40 | wantValidateErr: []error{ 41 | fmt.Errorf("Host: Array must have at least 1 items"), 42 | }, 43 | }, 44 | { 45 | caseDesc: "Test pattern match", 46 | givePath: "pattern.yaml", 47 | wantValidateErr: []error{ 48 | fmt.Errorf("Host.0: Does not match pattern '^http(s)?:\\/\\/[a-zA-Z0-9-_.:]+$'\nPrefix: Does not match pattern '^[\\/a-zA-Z0-9-_.]*$'"), 49 | fmt.Errorf("Prefix: Does not match pattern '^[\\/a-zA-Z0-9-_.]*$'\nHost.0: Does not match pattern '^http(s)?:\\/\\/[a-zA-Z0-9-_.:\\@]+$'"), 50 | }, 51 | }, 52 | { 53 | caseDesc: "Test minimum", 54 | givePath: "minimum.yaml", 55 | wantValidateErr: []error{ 56 | fmt.Errorf("Weight: Must be greater than or equal to 1\nTimeout.Connect: Must be greater than or equal to 1"), 57 | fmt.Errorf("Timeout.Connect: Must be greater than or equal to 1\nWeight: Must be greater than or equal to 1"), 58 | }, 59 | }, 60 | } 61 | 62 | for _, tc := range tests { 63 | bc := ReadFile(t, tc.givePath) 64 | _, err := nacosBuilder(bc) 65 | ret := false 66 | for _, wantErr := range tc.wantValidateErr { 67 | if wantErr.Error() == err.Error() { 68 | ret = true 69 | break 70 | } 71 | } 72 | assert.True(t, ret, tc.caseDesc) 73 | } 74 | } 75 | 76 | func TestNacosBuilder(t *testing.T) { 77 | tests := []struct { 78 | caseDesc string 79 | givePath string 80 | wantNacos *Nacos 81 | }{ 82 | { 83 | caseDesc: "Test Builder: default value", 84 | givePath: "default_value.yaml", 85 | wantNacos: &Nacos{ 86 | Host: []string{"http://127.0.0.1:8848"}, 87 | Weight: 100, 88 | Timeout: timeout{ 89 | Connect: 2000, 90 | Send: 2000, 91 | Read: 5000, 92 | }, 93 | }, 94 | }, 95 | { 96 | caseDesc: "Test Builder: set value", 97 | givePath: "set_value.yaml", 98 | wantNacos: &Nacos{ 99 | Host: []string{"http://127.0.0.1:8858"}, 100 | Prefix: "/nacos/v2/", 101 | Weight: 10, 102 | Timeout: timeout{ 103 | Connect: 200, 104 | Send: 200, 105 | Read: 500, 106 | }, 107 | }, 108 | }, 109 | } 110 | 111 | for _, tc := range tests { 112 | bc := ReadFile(t, tc.givePath) 113 | nacos, err := nacosBuilder(bc) 114 | assert.Nil(t, err) 115 | ret := reflect.DeepEqual(nacos, tc.wantNacos) 116 | assert.True(t, ret, tc.caseDesc) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/conf/zookeeper.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/api7/apisix-seed/internal/utils" 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | func init() { 9 | DisBuilders["zookeeper"] = zkBuilder 10 | } 11 | 12 | const zkConfSchema = ` 13 | { 14 | "type": "object", 15 | "properties": { 16 | "Hosts": { 17 | "type": "array", 18 | "minItems": 1, 19 | "items": { 20 | "type": "string", 21 | "pattern": "^[a-zA-Z0-9-_.:\\@]+$", 22 | "minLength": 2, 23 | "maxLength": 100 24 | } 25 | }, 26 | "Prefix": { 27 | "type": "string", 28 | "pattern": "^[\\/a-zA-Z0-9-_.]*$", 29 | "maxLength": 100 30 | }, 31 | "Weight": { 32 | "type": "integer", 33 | "minimum": 1, 34 | "default": 100 35 | }, 36 | "Timeout": { 37 | "type": "integer", 38 | "minimum": 1, 39 | "default": 100 40 | } 41 | }, 42 | "required": [ 43 | "Hosts" 44 | ] 45 | } 46 | ` 47 | 48 | type Zookeeper struct { 49 | Hosts []string 50 | Prefix string 51 | Weight int 52 | Timeout int 53 | } 54 | 55 | func zkBuilder(content []byte) (interface{}, error) { 56 | zookeeper := Zookeeper{ 57 | Weight: 100, 58 | Timeout: 10, 59 | } 60 | err := yaml.Unmarshal(content, &zookeeper) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | validator, err := utils.NewJsonSchemaValidator(zkConfSchema) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | if err = validator.Validate(zookeeper); err != nil { 71 | return nil, err 72 | } 73 | return &zookeeper, nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/conf/zookeeper_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var zkYamlConfig = ` 10 | hosts: 11 | - "127.0.0.1:2181" 12 | prefix: /zookeeper 13 | weight: 100 14 | timeout: 10 15 | ` 16 | 17 | func TestZkBuilderConfig(t *testing.T) { 18 | zkConfBuilder, ok := DisBuilders["zookeeper"] 19 | assert.Equal(t, true, ok) 20 | zkConf, err := zkConfBuilder([]byte(zkYamlConfig)) 21 | assert.Nil(t, err) 22 | assert.Equal(t, zkConf.(*Zookeeper).Hosts, []string{"127.0.0.1:2181"}) 23 | assert.Equal(t, zkConf.(*Zookeeper).Prefix, "/zookeeper") 24 | assert.Equal(t, zkConf.(*Zookeeper).Weight, 100) 25 | assert.Equal(t, zkConf.(*Zookeeper).Timeout, 10) 26 | } 27 | -------------------------------------------------------------------------------- /internal/core/components/rewriter.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | 7 | "github.com/api7/gopkg/pkg/log" 8 | 9 | "github.com/api7/apisix-seed/internal/core/message" 10 | 11 | "github.com/api7/apisix-seed/internal/core/storer" 12 | "github.com/api7/apisix-seed/internal/discoverer" 13 | ) 14 | 15 | type Rewriter struct { 16 | ctx context.Context 17 | cancel context.CancelFunc 18 | 19 | // Limit the number of simultaneously update 20 | sem chan struct{} 21 | 22 | Prefix string 23 | } 24 | 25 | func (r *Rewriter) Init() { 26 | r.ctx, r.cancel = context.WithCancel(context.TODO()) 27 | // the number of semaphore is referenced to https://github.com/golang/go/blob/go1.17.1/src/cmd/compile/internal/noder/noder.go#L38 28 | r.sem = make(chan struct{}, runtime.GOMAXPROCS(0)+10) 29 | 30 | // Watch for service updates from Discoverer 31 | for _, dis := range discoverer.GetDiscoverers() { 32 | msgCh := dis.Watch() 33 | go r.watch(msgCh) 34 | } 35 | } 36 | 37 | func (r *Rewriter) Close() { 38 | log.Info("Rewriter close") 39 | r.cancel() 40 | 41 | for _, dis := range discoverer.GetDiscoverers() { 42 | dis.Stop() 43 | } 44 | } 45 | 46 | func (r *Rewriter) watch(ch chan *message.Message) { 47 | for { 48 | select { 49 | case <-r.ctx.Done(): 50 | return 51 | case msg := <-ch: 52 | // hand watcher notify message 53 | _, entity, _ := storer.FromatKey(msg.Key, r.Prefix) 54 | if entity == "" { 55 | log.Errorf("key format Invaild: %s", msg.Key) 56 | return 57 | } 58 | if err := storer.GetStore(entity).UpdateNodes(r.ctx, msg); err != nil { 59 | log.Errorf("update nodes failed: %s", err) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/core/components/rewriter_test.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/api7/apisix-seed/internal/core/message" 8 | 9 | "github.com/api7/apisix-seed/internal/core/storer" 10 | "github.com/api7/apisix-seed/internal/discoverer" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | ) 14 | 15 | func TestRewriter(t *testing.T) { 16 | caseDesc := "sanity" 17 | // init discover 18 | // Init Mock Discoverer 19 | discoverer.Discoveries = map[string]discoverer.NewDiscoverFunc{ 20 | "mocks_dis": discoverer.NewDiscovererMock, 21 | } 22 | _ = discoverer.InitDiscoverer("mocks_dis", nil) 23 | 24 | watchCh := make(chan *message.Message, 1) 25 | discover := discoverer.GetDiscoverer("mocks_dis") 26 | mDiscover := discover.(*discoverer.MockInterface) 27 | mDiscover.On("Watch").Run(func(args mock.Arguments) {}).Return(watchCh) 28 | 29 | givenA6Str := `{ 30 | "uri": "/nacosWithNamespaceId/*", 31 | "upstream": { 32 | "service_name": "APISIX-NACOS", 33 | "type": "roundrobin", 34 | "discovery_type": "nacos", 35 | "discovery_args": { 36 | "group_name": "DEFAULT_GROUP" 37 | } 38 | } 39 | }` 40 | givenNodes := &message.Node{ 41 | Host: "1.1.1.1", 42 | Port: 8080, 43 | Weight: 1, 44 | } 45 | 46 | expectKey := "/prefix/mocks/1" 47 | expectA6Str := `{ 48 | "uri": "/nacosWithNamespaceId/*", 49 | "upstream": { 50 | "nodes": { 51 | "host":"1.1.1.1", 52 | "port": 8080, 53 | "weight": 1 54 | }, 55 | "_service_name": "APISIX-NACOS", 56 | "type": "roundrobin", 57 | "_discovery_type": "nacos", 58 | "discovery_args": { 59 | "group_name": "DEFAULT_GROUP" 60 | } 61 | } 62 | }` 63 | // mock new upstream nodes was found 64 | msg, err := message.NewMessage("/prefix/mocks/1", []byte(givenA6Str), 1, message.EventAdd, message.A6RoutesConf) 65 | assert.Nil(t, err, caseDesc) 66 | 67 | msg.InjectNodes(givenNodes) 68 | watchCh <- msg 69 | 70 | // mock rewrite 71 | mStg := &storer.MockInterface{} 72 | mStg.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 73 | assert.Equal(t, expectKey, args[0], caseDesc) 74 | assert.JSONEq(t, expectA6Str, args[1].(string), caseDesc) 75 | }).Return(nil) 76 | storer.ClrearStores() 77 | err = storer.InitStore("mocks", storer.GenericStoreOption{ 78 | BasePath: "/prefix/mocks", 79 | Prefix: "/prefix", 80 | }, mStg) 81 | assert.Nil(t, err, caseDesc) 82 | 83 | rewriter := Rewriter{ 84 | Prefix: "/prefix", 85 | } 86 | rewriter.Init() 87 | 88 | time.Sleep(time.Second) 89 | } 90 | -------------------------------------------------------------------------------- /internal/core/components/watcher.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "runtime" 7 | "sync" 8 | 9 | "github.com/api7/gopkg/pkg/log" 10 | 11 | "github.com/api7/apisix-seed/internal/core/message" 12 | 13 | "github.com/api7/apisix-seed/internal/core/storer" 14 | "github.com/api7/apisix-seed/internal/discoverer" 15 | ) 16 | 17 | type Watcher struct { 18 | ctx context.Context 19 | cancel context.CancelFunc 20 | 21 | // Limit the number of simultaneously query 22 | sem chan struct{} 23 | } 24 | 25 | // Init: load apisix config from etcd, query service from discovery 26 | func (w *Watcher) Init() error { 27 | // the number of semaphore is referenced to https://github.com/golang/go/blob/go1.17.1/src/cmd/compile/internal/noder/noder.go#L38 28 | w.sem = make(chan struct{}, runtime.GOMAXPROCS(0)+10) 29 | 30 | loadSuccess := true 31 | // List the initial information 32 | for _, s := range storer.GetStores() { 33 | //eg: query from etcd by prefix /apisix/routes/ 34 | msgs, err := s.List(message.ServiceFilter) 35 | if err != nil { 36 | log.Errorf("storer list error: %v", err) 37 | loadSuccess = false 38 | break 39 | } 40 | 41 | if len(msgs) == 0 { 42 | continue 43 | } 44 | wg := sync.WaitGroup{} 45 | wg.Add(len(msgs)) 46 | for _, msg := range msgs { 47 | w.sem <- struct{}{} 48 | go w.handleQuery(msg, &wg) 49 | } 50 | wg.Wait() 51 | } 52 | 53 | if !loadSuccess { 54 | return errors.New("failed to load all etcd resources") 55 | } 56 | return nil 57 | } 58 | 59 | // Watch: when updating route、service、upstream, query service from discovery 60 | func (w *Watcher) Watch() { 61 | w.ctx, w.cancel = context.WithCancel(context.TODO()) 62 | 63 | // Watch for entity updates from Storer 64 | for _, s := range storer.GetStores() { 65 | go w.handleWatch(s) 66 | } 67 | } 68 | 69 | func (w *Watcher) Close() { 70 | w.cancel() 71 | 72 | for _, s := range storer.GetStores() { 73 | s.Unwatch() 74 | } 75 | } 76 | 77 | // handleQuery: init and query the service from discovery by apisix's conf 78 | func (w *Watcher) handleQuery(msg *message.Message, wg *sync.WaitGroup) { 79 | defer func() { 80 | <-w.sem 81 | wg.Done() 82 | }() 83 | 84 | _ = discoverer.GetDiscoverer(msg.DiscoveryType()).Query(msg) 85 | } 86 | 87 | func (w *Watcher) handleWatch(s *storer.GenericStore) { 88 | ch := s.Watch() 89 | 90 | for { 91 | select { 92 | case <-w.ctx.Done(): 93 | return 94 | case msgs := <-ch: 95 | wg := sync.WaitGroup{} 96 | wg.Add(len(msgs)) 97 | for _, msg := range msgs { 98 | w.sem <- struct{}{} 99 | go w.handleValue(msg, &wg, s) 100 | } 101 | wg.Wait() 102 | } 103 | } 104 | } 105 | 106 | func (w *Watcher) handleValue(msg *message.Message, wg *sync.WaitGroup, s *storer.GenericStore) { 107 | defer func() { 108 | <-w.sem 109 | wg.Done() 110 | }() 111 | 112 | log.Infof("Watcher handle %d event: key=%s", msg.Action, msg.Key) 113 | switch msg.Action { 114 | case message.EventAdd: 115 | w.update(msg, s) 116 | case message.EventDelete: 117 | w.delete(msg, s) 118 | } 119 | } 120 | 121 | func (w *Watcher) update(msg *message.Message, s *storer.GenericStore) { 122 | if !message.ServiceFilter(msg) { 123 | if !msg.HasNodesAttr() { 124 | return 125 | } 126 | w.delete(msg, s) 127 | return 128 | } 129 | 130 | obj, ok := s.Store(msg.Key, msg) 131 | if !ok { 132 | // Obtains a new entity with service information 133 | log.Infof("Watcher obtains a new entity %s with service information", msg.Key) 134 | _ = discoverer.GetDiscoverer(msg.DiscoveryType()).Query(msg) 135 | return 136 | } 137 | 138 | oldMsg := obj.(*message.Message) 139 | if message.ServiceUpdate(oldMsg, msg) { 140 | // Updates the service information of existing entity 141 | log.Infof("Watcher updates the service information of existing entity %s", msg.Key) 142 | _ = discoverer.GetDiscoverer(msg.DiscoveryType()).Update(oldMsg, msg) 143 | return 144 | } 145 | 146 | if message.ServiceReplace(oldMsg, msg) { 147 | // Replaces the service information of existing entity 148 | log.Infof("Watcher replaces the service information of existing entity %s", msg.Key) 149 | 150 | _ = discoverer.GetDiscoverer(oldMsg.DiscoveryType()).Delete(oldMsg) 151 | _ = discoverer.GetDiscoverer(msg.DiscoveryType()).Query(msg) 152 | 153 | return 154 | } 155 | 156 | log.Infof("Watcher update version only, key: %s, version: %d", msg.Key, msg.Version) 157 | _ = discoverer.GetDiscoverer(msg.DiscoveryType()).Update(oldMsg, msg) 158 | } 159 | 160 | func (w *Watcher) delete(msg *message.Message, s *storer.GenericStore) { 161 | obj, ok := s.Delete(msg.Key) 162 | if !ok { 163 | return 164 | } 165 | // Deletes an existing entity 166 | delMsg := obj.(*message.Message) 167 | log.Infof("Watcher deletes an existing entity %s", delMsg.Key) 168 | _ = discoverer.GetDiscoverer(delMsg.DiscoveryType()).Delete(delMsg) 169 | } 170 | -------------------------------------------------------------------------------- /internal/core/components/watcher_test.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/api7/apisix-seed/internal/core/message" 9 | 10 | "github.com/api7/apisix-seed/internal/core/storer" 11 | "github.com/api7/apisix-seed/internal/discoverer" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | func TestWatcherInit(t *testing.T) { 17 | caseDesc := "test WatcherInit" 18 | 19 | givenOpt := storer.GenericStoreOption{ 20 | BasePath: "/prefix/mocks", 21 | Prefix: "/prefix", 22 | } 23 | 24 | givenKey := "/prefix/mocks/1" 25 | givenA6Str := `{ 26 | "uri": "/nacosWithNamespaceId/*", 27 | "upstream": { 28 | "service_name": "APISIX-NACOS", 29 | "type": "roundrobin", 30 | "discovery_type": "mock_nacos", 31 | "discovery_args": { 32 | "group_name": "DEFAULT_GROUP" 33 | } 34 | } 35 | }` 36 | 37 | // inject mock function 38 | mStg := &storer.MockInterface{} 39 | mStg.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 40 | assert.Equal(t, givenOpt.BasePath, args[0]) 41 | }).Return(func() []*message.Message { 42 | msg, err := message.NewMessage(givenKey, []byte(givenA6Str), 1, message.EventAdd, message.A6RoutesConf) 43 | assert.Nil(t, err, caseDesc) 44 | return []*message.Message{msg} 45 | }(), nil) 46 | 47 | storer.ClrearStores() 48 | // init store 49 | err := storer.InitStore("mocks", givenOpt, mStg) 50 | assert.Nil(t, err, caseDesc) 51 | 52 | discoverer.Discoveries = map[string]discoverer.NewDiscoverFunc{ 53 | "mock_nacos": discoverer.NewDiscovererMock, 54 | } 55 | _ = discoverer.InitDiscoverer("mock_nacos", nil) 56 | 57 | discover := discoverer.GetDiscoverer("mock_nacos") 58 | mDiscover := discover.(interface{}).(*discoverer.MockInterface) 59 | mDiscover.On("Query", mock.Anything).Run(func(args mock.Arguments) { 60 | msg := args[0].(*message.Message) 61 | assert.Equal(t, givenKey, msg.Key) 62 | assert.Equal(t, "APISIX-NACOS", msg.ServiceName()) 63 | assert.Equal(t, "mock_nacos", msg.DiscoveryType()) 64 | assert.Equal(t, "DEFAULT_GROUP", msg.DiscoveryArgs()["group_name"]) 65 | 66 | }).Return(nil) 67 | 68 | watcher := Watcher{} 69 | assert.Nil(t, watcher.Init()) 70 | } 71 | 72 | func TestWatcher_Init_error(t *testing.T) { 73 | // inject mock function 74 | routeStg := &storer.MockInterface{} 75 | routeStg.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 76 | }).Return([]*message.Message{}, errors.New("list prefix is not found")) 77 | 78 | serviceStg := &storer.MockInterface{} 79 | serviceStg.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 80 | }).Return([]*message.Message{}, nil) 81 | 82 | storer.ClrearStores() 83 | // init store 84 | optRoute := storer.GenericStoreOption{ 85 | BasePath: "/apisix/routes", 86 | Prefix: "/apisix", 87 | } 88 | optService := storer.GenericStoreOption{ 89 | BasePath: "/apisix/services", 90 | Prefix: "/apisix", 91 | } 92 | assert.Nil(t, storer.InitStore("mock1", optRoute, routeStg)) 93 | assert.Nil(t, storer.InitStore("mock2", optService, serviceStg)) 94 | 95 | watcher := Watcher{} 96 | err := watcher.Init() 97 | assert.EqualError(t, err, "failed to load all etcd resources") 98 | } 99 | 100 | func TestWatcherWatch(t *testing.T) { 101 | watchCh := make(chan []*message.Message) 102 | mStg := &storer.MockInterface{} 103 | mStg.On("Watch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {}).Return(watchCh) 104 | 105 | storer.ClrearStores() 106 | 107 | caseDesc := "Test watcher watch" 108 | givenOpt := storer.GenericStoreOption{ 109 | BasePath: "/prefix/mocks", 110 | Prefix: "/prefix", 111 | } 112 | err := storer.InitStore("mocks", givenOpt, mStg) 113 | assert.Nil(t, err, caseDesc) 114 | 115 | discoverer.Discoveries = map[string]discoverer.NewDiscoverFunc{ 116 | "mock_nacos": discoverer.NewDiscovererMock, 117 | "mock_zk": discoverer.NewDiscovererMock, 118 | } 119 | _ = discoverer.InitDiscoverer("mock_nacos", nil) 120 | nDiscover := discoverer.GetDiscoverer("mock_nacos").(*discoverer.MockInterface) 121 | _ = discoverer.InitDiscoverer("mock_zk", nil) 122 | zDiscover := discoverer.GetDiscoverer("mock_zk").(*discoverer.MockInterface) 123 | 124 | givenKey := "/prefix/mocks/1" 125 | 126 | nDiscover.On("Query", mock.Anything).Run(func(args mock.Arguments) { 127 | msg := args[0].(*message.Message) 128 | assert.Equal(t, "APISIX-NACOS", msg.ServiceName(), caseDesc) 129 | assert.Equal(t, "DEFAULT_GROUP", msg.DiscoveryArgs()["group_name"], caseDesc) 130 | }).Return(nil) 131 | 132 | nDiscover.On("Update", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 133 | oldMsg := args[0].(*message.Message) 134 | newMsg := args[1].(*message.Message) 135 | assert.Equal(t, "APISIX-NACOS", oldMsg.ServiceName(), caseDesc) 136 | assert.Equal(t, "DEFAULT_GROUP", oldMsg.DiscoveryArgs()["group_name"], caseDesc) 137 | assert.Equal(t, "APISIX-NACOS", newMsg.ServiceName(), caseDesc) 138 | assert.Equal(t, "NEWDEFAULT_GROUP", newMsg.DiscoveryArgs()["group_name"], caseDesc) 139 | }).Return(nil) 140 | nDiscover.On("Delete", mock.Anything).Run(func(args mock.Arguments) { 141 | msg := args[0].(*message.Message) 142 | assert.Equal(t, "APISIX-NACOS", msg.ServiceName(), caseDesc) 143 | assert.Equal(t, "NEWDEFAULT_GROUP", msg.DiscoveryArgs()["group_name"], caseDesc) 144 | }).Return(nil) 145 | 146 | zDiscover.On("Query", mock.Anything).Run(func(args mock.Arguments) { 147 | msg := args[0].(*message.Message) 148 | assert.Equal(t, "APISIX-ZK", msg.ServiceName(), caseDesc) 149 | }).Return(nil) 150 | zDiscover.On("Delete", mock.Anything).Run(func(args mock.Arguments) { 151 | msg := args[0].(*message.Message) 152 | assert.Equal(t, "APISIX-ZK", msg.ServiceName(), caseDesc) 153 | }).Return(nil) 154 | 155 | watcher := Watcher{} 156 | watcher.sem = make(chan struct{}, 10) 157 | watcher.Watch() 158 | 159 | givenA6Str := `{ 160 | "uri": "/hh/*", 161 | "upstream": { 162 | "service_name": "APISIX-NACOS", 163 | "type": "roundrobin", 164 | "discovery_type": "mock_nacos", 165 | "discovery_args": { 166 | "group_name": "DEFAULT_GROUP" 167 | } 168 | } 169 | }` 170 | queryMsg, err := message.NewMessage(givenKey, []byte(givenA6Str), 1, message.EventAdd, message.A6RoutesConf) 171 | assert.Nil(t, err, caseDesc) 172 | watchCh <- []*message.Message{queryMsg} 173 | 174 | givenUpdatedA6Str := `{ 175 | "uri": "/hh/*", 176 | "upstream": { 177 | "service_name": "APISIX-NACOS", 178 | "type": "roundrobin", 179 | "discovery_type": "mock_nacos", 180 | "discovery_args": { 181 | "group_name": "NEWDEFAULT_GROUP" 182 | } 183 | } 184 | }` 185 | updateMsg, err := message.NewMessage(givenKey, []byte(givenUpdatedA6Str), 1, message.EventAdd, message.A6RoutesConf) 186 | assert.Nil(t, err, caseDesc) 187 | watchCh <- []*message.Message{updateMsg} 188 | 189 | givenReplacedA6Str := `{ 190 | "uri": "/hh/*", 191 | "upstream": { 192 | "service_name": "APISIX-ZK", 193 | "type": "roundrobin", 194 | "discovery_type": "mock_zk" 195 | } 196 | }` 197 | replaceMsg, err := message.NewMessage(givenKey, []byte(givenReplacedA6Str), 1, message.EventAdd, message.A6RoutesConf) 198 | assert.Nil(t, err, caseDesc) 199 | watchCh <- []*message.Message{replaceMsg} 200 | 201 | deleteMsg, err := message.NewMessage(givenKey, nil, 1, message.EventDelete, message.A6RoutesConf) 202 | assert.Nil(t, err, caseDesc) 203 | watchCh <- []*message.Message{deleteMsg} 204 | 205 | time.Sleep(3 * time.Second) 206 | } 207 | -------------------------------------------------------------------------------- /internal/core/message/a6conf.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type UpstreamArg struct { 10 | NamespaceID string `json:"namespace_id,omitempty"` 11 | GroupName string `json:"group_name,omitempty"` 12 | Metadata map[string]interface{} `json:"metadata,omitempty"` 13 | } 14 | 15 | type Upstream struct { 16 | Nodes interface{} `json:"nodes,omitempty"` 17 | DiscoveryType string `json:"discovery_type,omitempty"` 18 | DupDiscoveryType string `json:"_discovery_type,omitempty"` 19 | DiscoveryArgs *UpstreamArg `json:"discovery_args,omitempty"` 20 | DupServiceName string `json:"_service_name,omitempty"` 21 | ServiceName string `json:"service_name,omitempty"` 22 | } 23 | 24 | const ( 25 | A6RoutesConf = 0 26 | A6UpstreamsConf = 1 27 | A6ServicesConf = 2 28 | ) 29 | 30 | func ToA6Type(prefix string) int { 31 | if strings.HasSuffix(prefix, "routes") { 32 | return A6RoutesConf 33 | } 34 | if strings.HasSuffix(prefix, "upstreams") { 35 | return A6UpstreamsConf 36 | } 37 | if strings.HasSuffix(prefix, "services") { 38 | return A6ServicesConf 39 | } 40 | return A6RoutesConf 41 | } 42 | 43 | type A6Conf interface { 44 | GetAll() *map[string]interface{} 45 | Inject(nodes interface{}) 46 | Marshal() ([]byte, error) 47 | GetUpstream() Upstream 48 | HasNodesAttr() bool 49 | } 50 | 51 | func NewA6Conf(value []byte, a6Type int) (A6Conf, error) { 52 | switch a6Type { 53 | case A6RoutesConf: 54 | return NewRoutes(value) 55 | case A6UpstreamsConf: 56 | return NewUpstreams(value) 57 | case A6ServicesConf: 58 | return NewServices(value) 59 | default: 60 | return NewRoutes(value) 61 | } 62 | } 63 | 64 | func unmarshal(data []byte, v A6Conf) error { 65 | err := json.Unmarshal(data, v) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | err = json.Unmarshal(data, v.GetAll()) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | // Embed the latest value into `all` map 79 | func embedElm(v reflect.Value, all map[string]interface{}) { 80 | if v.Kind() == reflect.Ptr { 81 | v = v.Elem() 82 | } 83 | 84 | typ := v.Type() 85 | fieldNum := typ.NumField() 86 | for i := 0; i < fieldNum; i++ { 87 | field := typ.Field(i) 88 | fieldName := field.Name 89 | tagName := strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") 90 | 91 | if fieldName == "All" && tagName == "-" { 92 | continue 93 | } 94 | 95 | if fieldName == "hasNodesAttr" { 96 | continue 97 | } 98 | 99 | val := v.FieldByName(fieldName) 100 | // ignore members without set values 101 | if val.IsZero() { 102 | continue 103 | } 104 | 105 | if fieldName == "DiscoveryType" || fieldName == "ServiceName" { 106 | all["_"+tagName] = val.Interface() 107 | delete(all, tagName) 108 | continue 109 | } 110 | 111 | if val.Kind() == reflect.Ptr { 112 | val = val.Elem() 113 | } 114 | 115 | if val.Kind() == reflect.Struct { 116 | // handle struct embedding 117 | if field.Anonymous { 118 | embedElm(val, all) 119 | } else { 120 | if _, ok := all[tagName]; !ok { 121 | all[tagName] = make(map[string]interface{}) 122 | } 123 | embedElm(val, all[tagName].(map[string]interface{})) 124 | } 125 | } else { 126 | all[tagName] = val.Interface() 127 | } 128 | } 129 | } 130 | 131 | type Upstreams struct { 132 | Upstream 133 | hasNodesAttr bool 134 | All map[string]interface{} `json:"-"` 135 | } 136 | 137 | func (ups *Upstreams) GetAll() *map[string]interface{} { 138 | return &ups.All 139 | } 140 | 141 | func (ups *Upstreams) Marshal() ([]byte, error) { 142 | embedElm(reflect.ValueOf(ups), ups.All) 143 | 144 | return json.Marshal(ups.All) 145 | } 146 | 147 | func (ups *Upstreams) Inject(nodes interface{}) { 148 | ups.Nodes = nodes 149 | } 150 | 151 | func (ups *Upstreams) GetUpstream() Upstream { 152 | return ups.Upstream 153 | } 154 | 155 | func (ups *Upstreams) HasNodesAttr() bool { 156 | return ups.hasNodesAttr 157 | } 158 | 159 | func NewUpstreams(value []byte) (A6Conf, error) { 160 | ups := &Upstreams{ 161 | All: make(map[string]interface{}), 162 | } 163 | err := unmarshal(value, ups) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | //We have to save the state of the nodes property after serializing the original data, 169 | // because it may be changed in subsequent logic 170 | if ups.Nodes != nil { 171 | ups.hasNodesAttr = true 172 | } 173 | 174 | return ups, nil 175 | } 176 | 177 | type Routes struct { 178 | Upstream Upstream `json:"upstream"` 179 | All map[string]interface{} `json:"-"` 180 | hasNodesAttr bool 181 | } 182 | 183 | func (routes *Routes) GetAll() *map[string]interface{} { 184 | return &routes.All 185 | } 186 | 187 | func (routes *Routes) Marshal() ([]byte, error) { 188 | embedElm(reflect.ValueOf(routes), routes.All) 189 | 190 | return json.Marshal(routes.All) 191 | } 192 | 193 | func (routes *Routes) Inject(nodes interface{}) { 194 | routes.Upstream.Nodes = nodes 195 | } 196 | 197 | func (routes *Routes) GetUpstream() Upstream { 198 | return routes.Upstream 199 | } 200 | 201 | func (routes *Routes) HasNodesAttr() bool { 202 | return routes.hasNodesAttr 203 | } 204 | 205 | func NewRoutes(value []byte) (A6Conf, error) { 206 | routes := &Routes{ 207 | All: make(map[string]interface{}), 208 | } 209 | err := unmarshal(value, routes) 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | if routes.Upstream.Nodes != nil { 215 | routes.hasNodesAttr = true 216 | } 217 | return routes, nil 218 | } 219 | 220 | type Services struct { 221 | Upstream Upstream `json:"upstream"` 222 | All map[string]interface{} `json:"-"` 223 | hasNodesAttr bool 224 | } 225 | 226 | func (services *Services) GetAll() *map[string]interface{} { 227 | return &services.All 228 | } 229 | 230 | func (services *Services) Marshal() ([]byte, error) { 231 | embedElm(reflect.ValueOf(services), services.All) 232 | 233 | return json.Marshal(services.All) 234 | } 235 | 236 | func (services *Services) Inject(nodes interface{}) { 237 | services.Upstream.Nodes = nodes 238 | } 239 | 240 | func (services *Services) GetUpstream() Upstream { 241 | return services.Upstream 242 | } 243 | 244 | func (services *Services) HasNodesAttr() bool { 245 | return services.hasNodesAttr 246 | } 247 | 248 | func NewServices(value []byte) (A6Conf, error) { 249 | services := &Services{ 250 | All: make(map[string]interface{}), 251 | } 252 | err := unmarshal(value, services) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | if services.Upstream.Nodes != nil { 258 | services.hasNodesAttr = true 259 | } 260 | 261 | return services, nil 262 | } 263 | -------------------------------------------------------------------------------- /internal/core/message/a6conf_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewA6Conf_Routes(t *testing.T) { 10 | testCases := []struct { 11 | desc string 12 | value string 13 | err string 14 | }{ 15 | { 16 | desc: "normal", 17 | value: `{ 18 | "uri": "/hh", 19 | "upstream": { 20 | "discovery_type": "nacos", 21 | "service_name": "APISIX-NACOS", 22 | "discovery_args": { 23 | "group_name": "DEFAULT_GROUP", 24 | "metadata":{ 25 | "version":"v1", 26 | "is_gray":true 27 | } 28 | } 29 | } 30 | }`, 31 | }, 32 | { 33 | desc: "error conf", 34 | value: `{ 35 | "uri": "/hh" 36 | "upstream": { 37 | "discovery_type": "nacos", 38 | "service_name": "APISIX-NACOS", 39 | "discovery_args": { 40 | "group_name": "DEFAULT_GROUP" 41 | } 42 | } 43 | }`, 44 | err: `invalid character '"' after object key:value pair`, 45 | }, 46 | } 47 | 48 | for _, v := range testCases { 49 | a6, err := NewA6Conf([]byte(v.value), A6RoutesConf) 50 | if v.err != "" { 51 | assert.Equal(t, v.err, err.Error(), v.desc) 52 | } else { 53 | assert.Nil(t, err, v.desc) 54 | assert.Equal(t, "nacos", a6.GetUpstream().DiscoveryType) 55 | assert.Equal(t, "APISIX-NACOS", a6.GetUpstream().ServiceName) 56 | assert.Equal(t, "v1", a6.GetUpstream().DiscoveryArgs.Metadata["version"]) 57 | assert.Equal(t, true, a6.GetUpstream().DiscoveryArgs.Metadata["is_gray"]) 58 | } 59 | 60 | } 61 | } 62 | 63 | func TestInject_Routes(t *testing.T) { 64 | givenA6Str := `{ 65 | "uri": "/hh", 66 | "upstream": { 67 | "discovery_type": "nacos", 68 | "service_name": "APISIX-NACOS", 69 | "discovery_args": { 70 | "group_name": "DEFAULT_GROUP" 71 | } 72 | } 73 | }` 74 | nodes := []*Node{ 75 | { 76 | Host: "192.168.1.1", 77 | Port: 80, 78 | Weight: 1, 79 | }, 80 | { 81 | Host: "192.168.1.2", 82 | Port: 80, 83 | Weight: 1, 84 | }, 85 | } 86 | caseDesc := "sanity" 87 | a6, err := NewA6Conf([]byte(givenA6Str), A6RoutesConf) 88 | assert.Nil(t, err, caseDesc) 89 | a6.Inject(nodes) 90 | assert.Len(t, a6.GetUpstream().Nodes, 2) 91 | } 92 | 93 | func TestMarshal_Routes(t *testing.T) { 94 | givenA6Str := `{ 95 | "status": 1, 96 | "id": "3", 97 | "uri": "/hh", 98 | "upstream": { 99 | "scheme": "http", 100 | "pass_host": "pass", 101 | "type": "roundrobin", 102 | "hash_on": "vars", 103 | "discovery_type": "nacos", 104 | "service_name": "APISIX-NACOS", 105 | "discovery_args": { 106 | "group_name": "DEFAULT_GROUP" 107 | } 108 | }, 109 | "create_time": 1648871506, 110 | "priority": 0, 111 | "update_time": 1648871506 112 | }` 113 | nodes := []*Node{ 114 | {Host: "192.168.1.1", Port: 80, Weight: 1}, 115 | {Host: "192.168.1.2", Port: 80, Weight: 1}, 116 | } 117 | 118 | wantA6Str := `{ 119 | "status": 1, 120 | "id": "3", 121 | "uri": "/hh", 122 | "upstream": { 123 | "scheme": "http", 124 | "pass_host": "pass", 125 | "type": "roundrobin", 126 | "hash_on": "vars", 127 | "_discovery_type": "nacos", 128 | "_service_name": "APISIX-NACOS", 129 | "discovery_args": { 130 | "group_name": "DEFAULT_GROUP" 131 | }, 132 | "nodes": [ 133 | { 134 | "host": "192.168.1.1", 135 | "port": 80, 136 | "weight": 1 137 | }, 138 | { 139 | "host": "192.168.1.2", 140 | "port": 80, 141 | "weight": 1 142 | } 143 | ] 144 | }, 145 | "create_time": 1648871506, 146 | "priority": 0, 147 | "update_time": 1648871506 148 | }` 149 | caseDesc := "sanity" 150 | a6, err := NewA6Conf([]byte(givenA6Str), A6RoutesConf) 151 | assert.Nil(t, err, caseDesc) 152 | 153 | a6.Inject(&nodes) 154 | ss, err := a6.Marshal() 155 | assert.Nil(t, err, caseDesc) 156 | 157 | assert.JSONEq(t, wantA6Str, string(ss)) 158 | } 159 | 160 | func TestHasNodesAttr_Routes(t *testing.T) { 161 | tests := []struct { 162 | name string 163 | a6Str string 164 | want bool 165 | }{ 166 | { 167 | name: "without upstream", 168 | a6Str: `{"plugins":{"fault-injection":{"abort":{"http_status":200,"body":"fine"}}},"uri":"/status"}`, 169 | want: false, 170 | }, 171 | { 172 | name: "has upstream without nodes", 173 | a6Str: `{"uri":"/hh","upstream":{"type":"roundrobin","discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 174 | want: false, 175 | }, 176 | { 177 | name: "normal", 178 | a6Str: `{"uri":"/hh","upstream":{"type":"roundrobin","nodes":[{"host":"192.168.1.1","port":80,"weight":1}]}}`, 179 | want: true, 180 | }, 181 | } 182 | for _, tt := range tests { 183 | t.Run(tt.name, func(t *testing.T) { 184 | routes, err := NewRoutes([]byte(tt.a6Str)) 185 | assert.Nil(t, err) 186 | assert.Equalf(t, tt.want, routes.HasNodesAttr(), "HasNodesAttr()") 187 | }) 188 | } 189 | } 190 | 191 | func TestNewA6Conf_Services(t *testing.T) { 192 | testCases := []struct { 193 | desc string 194 | value string 195 | err string 196 | }{ 197 | { 198 | desc: "normal", 199 | value: `{ 200 | "enable_websocket": false, 201 | "upstream": { 202 | "discovery_type": "nacos", 203 | "service_name": "APISIX-NACOS", 204 | "discovery_args": { 205 | "group_name": "DEFAULT_GROUP" 206 | } 207 | } 208 | }`, 209 | }, 210 | { 211 | desc: "error conf", 212 | value: `{ 213 | "enable_websocket": false 214 | "upstream": { 215 | "discovery_type": "nacos", 216 | "service_name": "APISIX-NACOS", 217 | "discovery_args": { 218 | "group_name": "DEFAULT_GROUP" 219 | } 220 | } 221 | }`, 222 | err: `invalid character '"' after object key:value pair`, 223 | }, 224 | } 225 | 226 | for _, v := range testCases { 227 | a6, err := NewA6Conf([]byte(v.value), A6ServicesConf) 228 | if v.err != "" { 229 | assert.Equal(t, v.err, err.Error(), v.desc) 230 | } else { 231 | assert.Nil(t, err, v.desc) 232 | assert.Equal(t, "nacos", a6.GetUpstream().DiscoveryType) 233 | assert.Equal(t, "APISIX-NACOS", a6.GetUpstream().ServiceName) 234 | } 235 | 236 | } 237 | } 238 | 239 | func TestInject_Services(t *testing.T) { 240 | givenA6Str := `{ 241 | "enable_websocket": false, 242 | "upstream": { 243 | "discovery_type": "nacos", 244 | "service_name": "APISIX-NACOS", 245 | "discovery_args": { 246 | "group_name": "DEFAULT_GROUP" 247 | } 248 | } 249 | }` 250 | nodes := []*Node{ 251 | { 252 | Host: "192.168.1.1", 253 | Port: 80, 254 | Weight: 1, 255 | }, 256 | { 257 | Host: "192.168.1.2", 258 | Port: 80, 259 | Weight: 1, 260 | }, 261 | } 262 | caseDesc := "sanity" 263 | a6, err := NewA6Conf([]byte(givenA6Str), A6ServicesConf) 264 | assert.Nil(t, err, caseDesc) 265 | a6.Inject(nodes) 266 | assert.Len(t, a6.GetUpstream().Nodes, 2) 267 | } 268 | 269 | func TestMarshal_Services(t *testing.T) { 270 | givenA6Str := `{ 271 | "enable_websocket": false, 272 | "upstream": { 273 | "scheme": "http", 274 | "pass_host": "pass", 275 | "type": "roundrobin", 276 | "hash_on": "vars", 277 | "discovery_type": "nacos", 278 | "service_name": "APISIX-NACOS", 279 | "discovery_args": { 280 | "group_name": "DEFAULT_GROUP" 281 | } 282 | }, 283 | "create_time": 1648871506, 284 | "update_time": 1648871506 285 | }` 286 | nodes := []*Node{ 287 | {Host: "192.168.1.1", Port: 80, Weight: 1}, 288 | {Host: "192.168.1.2", Port: 80, Weight: 1}, 289 | } 290 | 291 | wantA6Str := `{ 292 | "enable_websocket": false, 293 | "upstream": { 294 | "scheme": "http", 295 | "pass_host": "pass", 296 | "type": "roundrobin", 297 | "hash_on": "vars", 298 | "_discovery_type": "nacos", 299 | "_service_name": "APISIX-NACOS", 300 | "discovery_args": { 301 | "group_name": "DEFAULT_GROUP" 302 | }, 303 | "nodes": [ 304 | { 305 | "host": "192.168.1.1", 306 | "port": 80, 307 | "weight": 1 308 | }, 309 | { 310 | "host": "192.168.1.2", 311 | "port": 80, 312 | "weight": 1 313 | } 314 | ] 315 | }, 316 | "create_time": 1648871506, 317 | "update_time": 1648871506 318 | }` 319 | caseDesc := "sanity" 320 | a6, err := NewA6Conf([]byte(givenA6Str), A6RoutesConf) 321 | assert.Nil(t, err, caseDesc) 322 | 323 | a6.Inject(&nodes) 324 | ss, err := a6.Marshal() 325 | assert.Nil(t, err, caseDesc) 326 | 327 | assert.JSONEq(t, wantA6Str, string(ss)) 328 | } 329 | 330 | func TestHasNodesAttr_Services(t *testing.T) { 331 | tests := []struct { 332 | name string 333 | a6Str string 334 | want bool 335 | }{ 336 | { 337 | name: "without upstream", 338 | a6Str: `{"plugins":{"limit-count":{"count":2,"time_window":60,"rejected_code":503,"key":"remote_addr"}}}`, 339 | want: false, 340 | }, 341 | { 342 | name: "has upstream without nodes", 343 | a6Str: `{"plugins":{"limit-count":{"count":2,"time_window":60,"rejected_code":503,"key":"remote_addr"}},"upstream":{"type":"roundrobin","discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 344 | want: false, 345 | }, 346 | { 347 | name: "normal", 348 | a6Str: `{"plugins":{"limit-count":{"count":2,"time_window":60,"rejected_code":503,"key":"remote_addr"}},"upstream":{"type":"roundrobin","nodes":{"127.0.0.1:1980":1}}}`, 349 | want: true, 350 | }, 351 | } 352 | for _, tt := range tests { 353 | t.Run(tt.name, func(t *testing.T) { 354 | services, err := NewServices([]byte(tt.a6Str)) 355 | assert.Nil(t, err) 356 | assert.Equalf(t, tt.want, services.HasNodesAttr(), "HasNodesAttr()") 357 | }) 358 | } 359 | } 360 | 361 | func TestNewA6Conf_Upstreams(t *testing.T) { 362 | testCases := []struct { 363 | desc string 364 | value string 365 | err string 366 | }{ 367 | { 368 | desc: "normal", 369 | value: `{ 370 | "discovery_type": "nacos", 371 | "service_name": "APISIX-NACOS", 372 | "discovery_args": { 373 | "group_name": "DEFAULT_GROUP" 374 | } 375 | }`, 376 | }, 377 | { 378 | desc: "error conf", 379 | value: `{ 380 | "discovery_type": "nacos" 381 | "service_name": "APISIX-NACOS", 382 | "discovery_args": { 383 | "group_name": "DEFAULT_GROUP" 384 | } 385 | }`, 386 | err: `invalid character '"' after object key:value pair`, 387 | }, 388 | } 389 | 390 | for _, v := range testCases { 391 | a6, err := NewA6Conf([]byte(v.value), A6UpstreamsConf) 392 | if v.err != "" { 393 | assert.Equal(t, v.err, err.Error(), v.desc) 394 | } else { 395 | assert.Nil(t, err, v.desc) 396 | assert.Equal(t, "nacos", a6.GetUpstream().DiscoveryType) 397 | assert.Equal(t, "APISIX-NACOS", a6.GetUpstream().ServiceName) 398 | } 399 | 400 | } 401 | } 402 | 403 | func TestInject_Upstreams(t *testing.T) { 404 | givenA6Str := `{ 405 | "discovery_type": "nacos", 406 | "service_name": "APISIX-NACOS", 407 | "discovery_args": { 408 | "group_name": "DEFAULT_GROUP" 409 | } 410 | }` 411 | nodes := []*Node{ 412 | { 413 | Host: "192.168.1.1", 414 | Port: 80, 415 | Weight: 1, 416 | }, 417 | { 418 | Host: "192.168.1.2", 419 | Port: 80, 420 | Weight: 1, 421 | }, 422 | } 423 | caseDesc := "sanity" 424 | a6, err := NewA6Conf([]byte(givenA6Str), A6UpstreamsConf) 425 | assert.Nil(t, err, caseDesc) 426 | a6.Inject(nodes) 427 | assert.Len(t, a6.GetUpstream().Nodes, 2) 428 | } 429 | 430 | func TestMarshal_Upstreams(t *testing.T) { 431 | givenA6Str := `{ 432 | "status":1, 433 | "id":"3", 434 | "scheme":"http", 435 | "pass_host":"pass", 436 | "type":"roundrobin", 437 | "hash_on":"vars", 438 | "discovery_type":"nacos", 439 | "service_name":"APISIX-NACOS", 440 | "discovery_args":{ 441 | "group_name":"DEFAULT_GROUP", 442 | "metadata":{ 443 | "version":"v1" 444 | } 445 | }, 446 | "create_time":1648871506, 447 | "update_time":1648871506 448 | }` 449 | nodes := []*Node{ 450 | {Host: "192.168.1.1", Port: 80, Weight: 1}, 451 | {Host: "192.168.1.2", Port: 80, Weight: 1}, 452 | } 453 | 454 | wantA6Str := `{ 455 | "status": 1, 456 | "id": "3", 457 | "scheme": "http", 458 | "pass_host": "pass", 459 | "type": "roundrobin", 460 | "hash_on": "vars", 461 | "_discovery_type": "nacos", 462 | "_service_name": "APISIX-NACOS", 463 | "discovery_args": { 464 | "group_name": "DEFAULT_GROUP", 465 | "metadata":{ 466 | "version":"v1" 467 | } 468 | }, 469 | "nodes": [ 470 | { 471 | "host": "192.168.1.1", 472 | "port": 80, 473 | "weight": 1 474 | }, 475 | { 476 | "host": "192.168.1.2", 477 | "port": 80, 478 | "weight": 1 479 | } 480 | ], 481 | "create_time": 1648871506, 482 | "update_time": 1648871506 483 | }` 484 | caseDesc := "sanity" 485 | a6, err := NewA6Conf([]byte(givenA6Str), A6UpstreamsConf) 486 | assert.Nil(t, err, caseDesc) 487 | 488 | a6.Inject(&nodes) 489 | ss, err := a6.Marshal() 490 | assert.Nil(t, err, caseDesc) 491 | 492 | assert.JSONEq(t, wantA6Str, string(ss)) 493 | } 494 | 495 | func TestHasNodesAttr_Upstreams(t *testing.T) { 496 | tests := []struct { 497 | name string 498 | a6Str string 499 | want bool 500 | }{ 501 | { 502 | name: "upstream without nodes", 503 | a6Str: `{"type":"roundrobin","discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}`, 504 | want: false, 505 | }, 506 | { 507 | name: "normal", 508 | a6Str: `{"type":"roundrobin","nodes":{"127.0.0.1:1980":1}}`, 509 | want: true, 510 | }, 511 | } 512 | for _, tt := range tests { 513 | t.Run(tt.name, func(t *testing.T) { 514 | ups, err := NewUpstreams([]byte(tt.a6Str)) 515 | assert.Nil(t, err) 516 | assert.Equalf(t, tt.want, ups.HasNodesAttr(), "HasNodesAttr()") 517 | }) 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /internal/core/message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type StoreEvent = int 8 | 9 | const ( 10 | // add or update config 11 | EventAdd StoreEvent = 0x01 12 | // delete config 13 | EventDelete StoreEvent = 0102 14 | ) 15 | 16 | type Node struct { 17 | Host string `json:"host,omitempty"` 18 | Port int `json:"port,omitempty"` 19 | Weight int `json:"weight"` 20 | Metadata interface{} `json:"metadata,omitempty"` 21 | } 22 | 23 | type Message struct { 24 | Key string 25 | Value string 26 | Version int64 27 | Action StoreEvent 28 | a6Conf A6Conf 29 | } 30 | 31 | func NewMessage(key string, value []byte, version int64, action StoreEvent, a6Type int) (*Message, error) { 32 | msg := &Message{ 33 | Key: key, 34 | Value: string(value), 35 | Version: version, 36 | Action: action, 37 | } 38 | if len(value) != 0 { 39 | a6, err := NewA6Conf(value, a6Type) 40 | if err != nil { 41 | return nil, err 42 | } 43 | msg.a6Conf = a6 44 | } 45 | return msg, nil 46 | } 47 | 48 | func (msg *Message) ServiceName() string { 49 | up := msg.a6Conf.GetUpstream() 50 | if up.ServiceName != "" { 51 | return up.ServiceName 52 | } 53 | return up.DupServiceName 54 | } 55 | 56 | func (msg *Message) DiscoveryType() string { 57 | up := msg.a6Conf.GetUpstream() 58 | if up.DiscoveryType != "" { 59 | return up.DiscoveryType 60 | } 61 | return up.DupDiscoveryType 62 | } 63 | 64 | func (msg *Message) DiscoveryArgs() map[string]interface{} { 65 | up := msg.a6Conf.GetUpstream() 66 | if up.DiscoveryArgs == nil { 67 | return nil 68 | } 69 | return map[string]interface{}{ 70 | "namespace_id": up.DiscoveryArgs.NamespaceID, 71 | "group_name": up.DiscoveryArgs.GroupName, 72 | "metadata": up.DiscoveryArgs.Metadata, 73 | } 74 | } 75 | 76 | func (msg *Message) InjectNodes(nodes interface{}) { 77 | msg.a6Conf.Inject(nodes) 78 | } 79 | 80 | func (msg *Message) HasNodesAttr() bool { 81 | return msg.a6Conf.HasNodesAttr() 82 | } 83 | 84 | func (msg *Message) Marshal() ([]byte, error) { 85 | return msg.a6Conf.Marshal() 86 | } 87 | 88 | func ServiceFilter(msg *Message) bool { 89 | if msg.ServiceName() != "" && msg.DiscoveryType() != "" { 90 | return true 91 | } 92 | return false 93 | } 94 | 95 | func ServiceUpdate(msg, newMsg *Message) bool { 96 | if msg.ServiceName() != newMsg.ServiceName() || msg.DiscoveryType() != newMsg.DiscoveryType() { 97 | return false 98 | } 99 | 100 | // Two pointers are equal only when they are both nil 101 | args := msg.DiscoveryArgs() 102 | newArgs := newMsg.DiscoveryArgs() 103 | if args == nil && newArgs == nil { 104 | return false 105 | } 106 | if (args == nil && newArgs != nil) || (args != nil && newArgs == nil) { 107 | return true 108 | } 109 | if args["group_name"] != newArgs["group_name"] || 110 | args["namespace_id"] != newArgs["namespace_id"] || 111 | !reflect.DeepEqual(args["metadata"], newArgs["metadata"]) { 112 | return true 113 | } 114 | 115 | return false 116 | } 117 | 118 | func ServiceReplace(msg, newMsg *Message) bool { 119 | return msg.ServiceName() != newMsg.ServiceName() || 120 | msg.DiscoveryType() != newMsg.DiscoveryType() 121 | } 122 | -------------------------------------------------------------------------------- /internal/core/message/message_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMessage(t *testing.T) { 10 | givenA6Str := `{ 11 | "status": 1, 12 | "id": "3", 13 | "uri": "/hh", 14 | "upstream": { 15 | "scheme": "http", 16 | "pass_host": "pass", 17 | "type": "roundrobin", 18 | "hash_on": "vars", 19 | "_discovery_type": "nacos", 20 | "service_name": "APISIX-NACOS", 21 | "discovery_args": { 22 | "group_name": "DEFAULT_GROUP", 23 | "metadata":{ 24 | "version":"v1", 25 | "is_gray":true 26 | } 27 | } 28 | }, 29 | "create_time": 1648871506, 30 | "priority": 0, 31 | "update_time": 1648871506 32 | }` 33 | givenKey := "/apisix/routes/1" 34 | givenAction := EventAdd 35 | caseDesc := "normal" 36 | 37 | msg, err := NewMessage(givenKey, []byte(givenA6Str), 1, givenAction, A6RoutesConf) 38 | assert.Nil(t, err, caseDesc) 39 | 40 | assert.Equal(t, givenKey, msg.Key, caseDesc) 41 | assert.Equal(t, givenAction, msg.Action, caseDesc) 42 | assert.Equal(t, "nacos", msg.DiscoveryType(), caseDesc) 43 | assert.Equal(t, "APISIX-NACOS", msg.ServiceName(), caseDesc) 44 | assert.Equal(t, "DEFAULT_GROUP", msg.DiscoveryArgs()["group_name"], caseDesc) 45 | assert.Equal(t, "v1", msg.DiscoveryArgs()["metadata"].(map[string]interface{})["version"], caseDesc) 46 | assert.Equal(t, true, msg.DiscoveryArgs()["metadata"].(map[string]interface{})["is_gray"], caseDesc) 47 | assert.Equal(t, false, msg.HasNodesAttr(), caseDesc) 48 | 49 | msg.InjectNodes([]*Node{ 50 | {Host: "1.1.31.1", Port: 80, Weight: 1}, 51 | }) 52 | 53 | _, err = msg.Marshal() 54 | assert.Nil(t, err, caseDesc) 55 | } 56 | 57 | func TestServiceFilter(t *testing.T) { 58 | testCases := []struct { 59 | desc string 60 | key string 61 | value string 62 | ret bool 63 | }{ 64 | { 65 | desc: "normal", 66 | key: "/apisix/routes/a", 67 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 68 | ret: true, 69 | }, 70 | { 71 | desc: "no service_name", 72 | key: "/apisix/routes/b", 73 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 74 | ret: false, 75 | }, 76 | } 77 | for _, tc := range testCases { 78 | msg, err := NewMessage(tc.key, []byte(tc.value), 1, EventAdd, A6RoutesConf) 79 | assert.Nil(t, err, tc.desc) 80 | assert.Equal(t, tc.ret, ServiceFilter(msg)) 81 | } 82 | } 83 | 84 | func TestServiceUpdate(t *testing.T) { 85 | testCases := []struct { 86 | desc string 87 | key string 88 | value string 89 | newValue string 90 | ret bool 91 | }{ 92 | { 93 | desc: "a6 conf no change", 94 | key: "/apisix/routes/a", 95 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 96 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 97 | ret: false, 98 | }, 99 | { 100 | desc: "service_name changed", 101 | key: "/apisix/routes/b", 102 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 103 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"zk","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 104 | ret: false, 105 | }, 106 | { 107 | desc: "args changed", 108 | key: "/apisix/routes/b", 109 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 110 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"NEW-DEFAULT_GROUP"}}}`, 111 | ret: true, 112 | }, 113 | { 114 | desc: "metadata changed", 115 | key: "/apisix/routes/b", 116 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP","metadata":{"version":"v1"}}}}`, 117 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP","metadata":{"version":"v1"}}}}`, 118 | ret: false, 119 | }, 120 | { 121 | desc: "metadata changed", 122 | key: "/apisix/routes/b", 123 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP","metadata":{"version":"v1"}}}}`, 124 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP","metadata":{"version":"v2"}}}}`, 125 | ret: true, 126 | }, 127 | } 128 | for _, tc := range testCases { 129 | msg, err := NewMessage(tc.key, []byte(tc.value), 1, EventAdd, A6RoutesConf) 130 | assert.Nil(t, err, tc.desc) 131 | newMsg, err := NewMessage(tc.key, []byte(tc.newValue), 1, EventAdd, A6RoutesConf) 132 | assert.Nil(t, err, tc.desc) 133 | assert.Equal(t, tc.ret, ServiceUpdate(msg, newMsg), tc.desc) 134 | } 135 | } 136 | 137 | func TestServiceReplace(t *testing.T) { 138 | testCases := []struct { 139 | desc string 140 | key string 141 | value string 142 | newValue string 143 | ret bool 144 | }{ 145 | { 146 | desc: "a6 conf no change", 147 | key: "/apisix/routes/a", 148 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 149 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 150 | ret: false, 151 | }, 152 | { 153 | desc: "service_name changed", 154 | key: "/apisix/routes/b", 155 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 156 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"zk","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 157 | ret: true, 158 | }, 159 | { 160 | desc: "args changed", 161 | key: "/apisix/routes/b", 162 | value: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP"}}}`, 163 | newValue: `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"APISIX-NACOS","discovery_args":{"group_name":"DEFAULT_GROUP","metadata":{"version":"v2"}}}}`, 164 | ret: false, 165 | }, 166 | } 167 | for _, tc := range testCases { 168 | msg, err := NewMessage(tc.key, []byte(tc.value), 1, EventAdd, A6RoutesConf) 169 | assert.Nil(t, err, tc.desc) 170 | newMsg, err := NewMessage(tc.key, []byte(tc.newValue), 1, EventAdd, A6RoutesConf) 171 | assert.Nil(t, err, tc.desc) 172 | assert.Equal(t, tc.ret, ServiceReplace(msg, newMsg), tc.desc) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /internal/core/storer/etcd.go: -------------------------------------------------------------------------------- 1 | package storer 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/api7/gopkg/pkg/log" 10 | 11 | "github.com/api7/apisix-seed/internal/core/message" 12 | 13 | "github.com/api7/apisix-seed/internal/conf" 14 | 15 | "go.etcd.io/etcd/client/pkg/v3/transport" 16 | clientv3 "go.etcd.io/etcd/client/v3" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | var ( 21 | DirPlaceholder = []byte("init_dir") 22 | ) 23 | 24 | type EtcdV3 struct { 25 | client *clientv3.Client 26 | conf clientv3.Config 27 | timeout time.Duration 28 | } 29 | 30 | func NewEtcd(etcdConf *conf.Etcd) (*EtcdV3, error) { 31 | timeout := time.Duration(etcdConf.Timeout) 32 | s := &EtcdV3{timeout: timeout} 33 | 34 | if s.timeout == 0 { 35 | s.timeout = 10 * time.Second 36 | } 37 | 38 | config := clientv3.Config{ 39 | Endpoints: etcdConf.Host, 40 | DialTimeout: timeout, 41 | DialKeepAliveTimeout: timeout, 42 | Username: etcdConf.User, 43 | Password: etcdConf.Password, 44 | } 45 | 46 | if etcdConf.TLS != nil && etcdConf.TLS.Verify { 47 | tlsInfo := transport.TLSInfo{ 48 | CertFile: etcdConf.TLS.CertFile, 49 | KeyFile: etcdConf.TLS.KeyFile, 50 | } 51 | tlsConf, err := tlsInfo.ClientConfig() 52 | if err != nil { 53 | return nil, err 54 | } 55 | config.TLS = tlsConf 56 | } 57 | 58 | s.conf = config 59 | if err := s.init(); err != nil { 60 | return nil, err 61 | } 62 | 63 | return s, nil 64 | } 65 | 66 | func (s *EtcdV3) init() error { 67 | cli, err := clientv3.New(s.conf) 68 | if err != nil { 69 | log.Errorf("etcd init failed: %s", err) 70 | return err 71 | } 72 | 73 | s.client = cli 74 | return nil 75 | } 76 | 77 | // Get a value given its key 78 | func (s *EtcdV3) Get(ctx context.Context, key string) (string, error) { 79 | ctx, cancel := context.WithTimeout(ctx, s.timeout) 80 | defer cancel() 81 | 82 | resp, err := s.client.Get(ctx, key) 83 | if err != nil { 84 | log.Errorf("etcd get key[%s] failed: %s", key, err) 85 | return "", fmt.Errorf("etcd get key[%s] failed: %s", key, err) 86 | } 87 | if resp.Count == 0 { 88 | log.Warnf("etcd get key[%s] is not found", key) 89 | return "", fmt.Errorf("etcd get key[%s] is not found", key) 90 | } 91 | 92 | return string(resp.Kvs[0].Value), nil 93 | } 94 | 95 | // List the content of a given prefix 96 | func (s *EtcdV3) List(ctx context.Context, prefix string) ([]*message.Message, error) { 97 | ctx, cancel := context.WithTimeout(ctx, s.timeout) 98 | defer cancel() 99 | 100 | resp, err := s.client.Get(ctx, prefix, clientv3.WithPrefix()) 101 | if err != nil { 102 | log.Errorf("etcd list prefix[%s] failed: %s", prefix, err) 103 | return nil, fmt.Errorf("etcd list prefix[%s] failed: %s", prefix, err) 104 | } 105 | if resp.Count == 0 { 106 | log.Warnf("etcd list prefix[%s] is not found", prefix) 107 | return nil, fmt.Errorf("etcd list prefix[%s] is not found", prefix) 108 | } 109 | 110 | // We use a placeholder to mark a key to be a directory. So we need to skip the hack here. 111 | if bytes.Equal(resp.Kvs[0].Value, DirPlaceholder) { 112 | resp.Kvs = resp.Kvs[1:] 113 | } 114 | 115 | msgs := make([]*message.Message, 0, len(resp.Kvs)) 116 | for _, kv := range resp.Kvs { 117 | msg, err := message.NewMessage(string(kv.Key), kv.Value, kv.Version, message.EventAdd, message.ToA6Type(prefix)) 118 | if err != nil { 119 | log.Errorf("etcd list prefix[%s] format failed: %s", prefix, err) 120 | continue 121 | } 122 | msgs = append(msgs, msg) 123 | } 124 | 125 | return msgs, nil 126 | } 127 | 128 | // Create a value at the specified key 129 | func (s *EtcdV3) Create(ctx context.Context, key, value string) error { 130 | ctx, cancel := context.WithTimeout(ctx, s.timeout) 131 | defer cancel() 132 | 133 | _, err := s.client.Put(ctx, key, value) 134 | if err != nil { 135 | log.Errorf("etcd put key[%s] failed: %s", key, err) 136 | return fmt.Errorf("etcd put key[%s] failed: %s", key, err) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // Update a value at the specified key 143 | func (s *EtcdV3) Update(ctx context.Context, key, value string, version int64) error { 144 | ctx, cancel := context.WithTimeout(ctx, s.timeout) 145 | defer cancel() 146 | 147 | txn := s.client.Txn(ctx). 148 | If(clientv3.Compare(clientv3.Version(key), "=", version)). 149 | Then(clientv3.OpPut(key, value)) 150 | 151 | resp, err := txn.Commit() 152 | if err != nil { 153 | log.Errorf("etcd update key[%s] failed: %s", key, err) 154 | return fmt.Errorf("etcd update key[%s] failed: %s", key, err) 155 | } 156 | if !resp.Succeeded { 157 | log.Infof("key[%s] may have been updated by other instances", key) 158 | } 159 | log.Infof("etcd update key[%s], version: %d", key, version) 160 | return nil 161 | } 162 | 163 | // Delete a value at the specified key 164 | func (s *EtcdV3) Delete(ctx context.Context, key string) error { 165 | ctx, cancel := context.WithTimeout(ctx, s.timeout) 166 | defer cancel() 167 | 168 | resp, err := s.client.Delete(ctx, key) 169 | if err != nil { 170 | log.Errorf("etcd delete key[%s] failed: %s", key, err) 171 | return fmt.Errorf("etcd delete key[%s] failed: %s", key, err) 172 | } 173 | if resp.Deleted == 0 { 174 | log.Warnf("etcd delete key[%s] is not found", key) 175 | return fmt.Errorf("etcd delete key[%s] is not found", key) 176 | } 177 | 178 | return nil 179 | } 180 | 181 | // DeletePrefix deletes a range of keys under a given prefix 182 | func (s *EtcdV3) DeletePrefix(ctx context.Context, prefix string) error { 183 | ctx, cancel := context.WithTimeout(ctx, s.timeout) 184 | defer cancel() 185 | 186 | resp, err := s.client.Delete(ctx, prefix, clientv3.WithPrefix()) 187 | if err != nil { 188 | log.Errorf("etcd delete prefix[%s] failed: %s", prefix, err) 189 | return fmt.Errorf("etcd delete prefix[%s] failed: %s", prefix, err) 190 | } 191 | if resp.Deleted == 0 { 192 | log.Warnf("etcd delete prefix[%s] is not found", prefix) 193 | return fmt.Errorf("etcd delete prefix[%s] is not found", prefix) 194 | } 195 | 196 | return nil 197 | } 198 | 199 | // Watch for changes on a key 200 | func (s *EtcdV3) Watch(ctx context.Context, prefix string) <-chan []*message.Message { 201 | eventChan := s.client.Watch(ctx, prefix, clientv3.WithPrefix()) 202 | ch := make(chan []*message.Message, 1) 203 | 204 | go func() { 205 | defer close(ch) 206 | 207 | for event := range eventChan { 208 | msgs := make([]*message.Message, 0, 16) 209 | if event.Err() != nil { 210 | log.Errorw("etcd watch error", 211 | zap.String("watch key", prefix), 212 | zap.Error(event.Err()), 213 | ) 214 | close(ch) 215 | return 216 | } 217 | for _, ev := range event.Events { 218 | // We use a placeholder to mark a key to be a directory. So we need to skip the hack here. 219 | if bytes.Equal(ev.Kv.Value, DirPlaceholder) { 220 | continue 221 | } 222 | 223 | key := string(ev.Kv.Key) 224 | 225 | var typ message.StoreEvent 226 | switch ev.Type { 227 | case clientv3.EventTypePut: 228 | typ = message.EventAdd 229 | case clientv3.EventTypeDelete: 230 | typ = message.EventDelete 231 | } 232 | 233 | log.Infof("watch changed, key: %s, version: %d", key, ev.Kv.Version) 234 | msg, err := message.NewMessage(key, ev.Kv.Value, ev.Kv.Version, typ, message.ToA6Type(prefix)) 235 | if err != nil { 236 | log.Warnf("etcd watch key[%s]'s %d event failed: %s", key, typ, err.Error()) 237 | continue 238 | } 239 | msgs = append(msgs, msg) 240 | } 241 | 242 | ch <- msgs 243 | } 244 | }() 245 | 246 | return ch 247 | } 248 | 249 | // Close the client connection 250 | func (s *EtcdV3) Close() error { 251 | if err := s.client.Close(); err != nil { 252 | log.Errorf("etcd client close failed: %s", err) 253 | return err 254 | } 255 | return nil 256 | } 257 | -------------------------------------------------------------------------------- /internal/core/storer/etcd_test.go: -------------------------------------------------------------------------------- 1 | package storer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/api7/apisix-seed/internal/conf" 11 | "github.com/api7/apisix-seed/internal/core/message" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var host = "localhost:2379" // nolint:unused 16 | 17 | func TestEtcdV3(t *testing.T) { 18 | // Make sure that etcd is installed in your test environment 19 | // Then comment out the following statement 20 | 21 | client, err := NewEtcd(&conf.Etcd{Host: []string{host}}) 22 | assert.Nil(t, err, "Test create etcd client") 23 | 24 | testCommon(t, client) 25 | testList(t, client) 26 | testWatch(t, client) 27 | 28 | err = client.Close() 29 | assert.Nil(t, err, "Test close etcd client") 30 | } 31 | 32 | // nolint:unused 33 | func testCommon(t *testing.T, client *EtcdV3) { 34 | value, newValue := "test_value", "new_test_value" 35 | for _, key := range []string{ 36 | "/apisix", 37 | "/apisix/routes", 38 | "/apisix/routes/1", 39 | } { 40 | // Create the key/value 41 | err := client.Create(context.Background(), key, value) 42 | assert.Nil(t, err, "Test create key") 43 | 44 | // Get should return the value 45 | val, err := client.Get(context.Background(), key) 46 | assert.Nil(t, err, "Test key get") 47 | assert.Equal(t, value, val, "Test get key value") 48 | 49 | // Update the key/value 50 | err = client.Update(context.Background(), key, newValue, 1) 51 | assert.Nil(t, err, "Test key update") 52 | 53 | // Get should return the new value 54 | val, err = client.Get(context.Background(), key) 55 | assert.Nil(t, err, "Test key get") 56 | assert.Equal(t, newValue, val, "Test get key: new value") 57 | 58 | // Delete the key 59 | err = client.Delete(context.Background(), key) 60 | assert.Nil(t, err, "Test key delete") 61 | 62 | // Delete the non-existing key 63 | err = client.Delete(context.Background(), key) 64 | wantErr := fmt.Errorf("etcd delete key[%s] is not found", key) 65 | assert.Equal(t, wantErr, err, "Test delete non-existing key") 66 | 67 | // Get should fail 68 | _, err = client.Get(context.Background(), key) 69 | wantErr = fmt.Errorf("etcd get key[%s] is not found", key) 70 | assert.Equal(t, wantErr, err, "Test get non-existing key") 71 | } 72 | } 73 | 74 | // nolint:unused 75 | func testList(t *testing.T, client *EtcdV3) { 76 | prefix := "testList" 77 | kvs := map[string]string{ 78 | "testList/first": getA6Conf("first"), 79 | "testList/second": getA6Conf("second"), 80 | "testList/": "init_dir", 81 | } 82 | 83 | for k, v := range kvs { 84 | err := client.Create(context.Background(), k, v) 85 | assert.Nil(t, err) 86 | } 87 | 88 | for _, parent := range []string{prefix, prefix + "/"} { 89 | pairs, err := client.List(context.Background(), parent) 90 | assert.Nil(t, err, "Test list prefix") 91 | assert.Len(t, pairs, 2, "Test list content") 92 | 93 | for _, pair := range pairs { 94 | if pair.Key == "dirPlaceholderKey" { 95 | assert.Fail(t, "should be skipped") 96 | } 97 | assert.Equal(t, kvs[pair.Key], pair.Value) 98 | } 99 | } 100 | 101 | err := client.DeletePrefix(context.Background(), prefix) 102 | assert.Nil(t, err, "Test delete prefix") 103 | 104 | // List should fail 105 | wantErr := fmt.Errorf("etcd list prefix[%s] is not found", prefix) 106 | pairs, err := client.List(context.Background(), prefix) 107 | assert.Equal(t, wantErr, err, "Test list non-existing prefix") 108 | assert.Nil(t, pairs) 109 | } 110 | 111 | // nolint:unused 112 | func testWatch(t *testing.T, client *EtcdV3) { 113 | prefix := "testWatch" 114 | key, value := "testWatch/node", getA6Conf("node") 115 | dirPlaceholderKey, dirPlaceholderValue := "testWatch/", "init_dir" 116 | 117 | msgsCh := client.Watch(context.Background(), prefix) 118 | // update loop 119 | wg := sync.WaitGroup{} 120 | wg.Add(1) 121 | go func() { 122 | defer wg.Done() 123 | time.Sleep(500 * time.Millisecond) 124 | 125 | err := client.Create(context.Background(), key, value) 126 | assert.Nil(t, err) 127 | 128 | err = client.Delete(context.Background(), key) 129 | assert.Nil(t, err) 130 | 131 | err = client.Create(context.Background(), dirPlaceholderKey, dirPlaceholderValue) 132 | assert.Nil(t, err) 133 | }() 134 | 135 | eventCount := 0 136 | addFlag, delFlag := false, false 137 | for { 138 | select { 139 | case msgs := <-msgsCh: 140 | eventCount += len(msgs) 141 | assert.True(t, eventCount < 3) 142 | for _, msg := range msgs { 143 | switch msg.Action { 144 | case message.EventAdd: 145 | addFlag = true 146 | case message.EventDelete: 147 | delFlag = true 148 | } 149 | assert.Equal(t, key, msg.Key) 150 | } 151 | if addFlag && delFlag { 152 | wg.Wait() 153 | return 154 | } 155 | case <-time.After(5 * time.Second): 156 | assert.True(t, false, "Test watch timeout reached") 157 | return 158 | } 159 | } 160 | 161 | } 162 | 163 | func getA6Conf(uri string) string { 164 | a6fmt := `{ 165 | "uri": "%s", 166 | "upstream": { 167 | "discovery_type": "nacos", 168 | "service_name": "APISIX-NACOS", 169 | "discovery_args": { 170 | "group_name": "DEFAULT_GROUP" 171 | } 172 | } 173 | }` 174 | return fmt.Sprintf(a6fmt, uri) 175 | } 176 | 177 | func TestConcurrencyUpdate(t *testing.T) { 178 | client, err := NewEtcd(&conf.Etcd{Host: []string{host}}) 179 | assert.Nil(t, err, "Test concurrency update etcd") 180 | 181 | ready := make(chan struct{}) 182 | done := make(chan struct{}) 183 | key, value := "testKey", "testValue" 184 | go func() { 185 | wg := sync.WaitGroup{} 186 | for i := 0; i < 10; i++ { 187 | wg.Add(1) 188 | go func() { 189 | defer wg.Done() 190 | <-ready 191 | err := client.Update(context.Background(), key, value, 0) 192 | assert.Nil(t, err) 193 | }() 194 | } 195 | wg.Wait() 196 | close(done) 197 | }() 198 | 199 | close(ready) 200 | <-done 201 | 202 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 203 | defer cancel() 204 | resp, err := client.client.Get(ctx, key) 205 | assert.Nil(t, err) 206 | assert.True(t, resp.Count > 0) 207 | 208 | assert.True(t, resp.Kvs[0].Version == 1) 209 | } 210 | -------------------------------------------------------------------------------- /internal/core/storer/store.go: -------------------------------------------------------------------------------- 1 | package storer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/api7/gopkg/pkg/log" 10 | 11 | "github.com/api7/apisix-seed/internal/core/message" 12 | ) 13 | 14 | type Interface interface { 15 | List(context.Context, string) ([]*message.Message, error) 16 | Update(context.Context, string, string, int64) error 17 | Watch(context.Context, string) <-chan []*message.Message 18 | } 19 | 20 | type GenericStoreOption struct { 21 | BasePath string 22 | Prefix string 23 | } 24 | 25 | type GenericStore struct { 26 | Typ string 27 | Stg Interface 28 | 29 | cache sync.Map 30 | opt GenericStoreOption 31 | 32 | cancel context.CancelFunc 33 | } 34 | 35 | func NewGenericStore(typ string, opt GenericStoreOption, stg Interface) (*GenericStore, error) { 36 | if opt.BasePath == "" { 37 | log.Error("base path empty") 38 | return nil, fmt.Errorf("base path can not be empty") 39 | } 40 | 41 | s := &GenericStore{ 42 | Typ: typ, 43 | Stg: stg, 44 | opt: opt, 45 | } 46 | 47 | return s, nil 48 | } 49 | 50 | func (s *GenericStore) List(filter func(*message.Message) bool) ([]*message.Message, error) { 51 | lc, lcancel := context.WithTimeout(context.TODO(), 5*time.Second) 52 | defer lcancel() 53 | ret, err := s.Stg.List(lc, s.opt.BasePath) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | objPtrs := make([]*message.Message, 0) 59 | for i := range ret { 60 | if filter == nil || filter(ret[i]) { 61 | s.Store(ret[i].Key, ret[i]) 62 | objPtrs = append(objPtrs, ret[i]) 63 | } 64 | } 65 | 66 | return objPtrs, nil 67 | } 68 | 69 | func (s *GenericStore) Watch() <-chan []*message.Message { 70 | c, cancel := context.WithCancel(context.TODO()) 71 | s.cancel = cancel 72 | 73 | ch := s.Stg.Watch(c, s.opt.BasePath) 74 | 75 | return ch 76 | } 77 | 78 | func (s *GenericStore) Unwatch() { 79 | s.cancel() 80 | } 81 | 82 | func (s *GenericStore) UpdateNodes(ctx context.Context, msg *message.Message) (err error) { 83 | bs, err := msg.Marshal() 84 | if err != nil { 85 | log.Errorf("json marshal failed: %s", err) 86 | return fmt.Errorf("marshal failed: %s", err) 87 | } 88 | if err = s.Stg.Update(ctx, msg.Key, string(bs), msg.Version); err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (s *GenericStore) Store(key string, objPtr interface{}) (interface{}, bool) { 96 | oldObj, ok := s.cache.LoadOrStore(key, objPtr) 97 | if ok { 98 | s.cache.Store(key, objPtr) 99 | } 100 | return oldObj, ok 101 | } 102 | 103 | func (s *GenericStore) Delete(key string) (interface{}, bool) { 104 | return s.cache.LoadAndDelete(key) 105 | } 106 | 107 | func (s *GenericStore) BasePath() string { 108 | return s.opt.BasePath 109 | } 110 | -------------------------------------------------------------------------------- /internal/core/storer/store_mock.go: -------------------------------------------------------------------------------- 1 | package storer 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/api7/apisix-seed/internal/core/message" 7 | 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | type MockInterface struct { 12 | mock.Mock 13 | } 14 | 15 | func (m *MockInterface) List(_ context.Context, key string) ([]*message.Message, error) { 16 | ret := m.Called(key) 17 | return ret.Get(0).([]*message.Message), ret.Error(1) 18 | } 19 | 20 | func (m *MockInterface) Update(_ context.Context, key, value string, version int64) error { 21 | ret := m.Called(key, value, version) 22 | return ret.Error(0) 23 | } 24 | 25 | func (m *MockInterface) Watch(_ context.Context, key string) <-chan []*message.Message { 26 | ret := m.Called(key) 27 | return ret.Get(0).(chan []*message.Message) 28 | } 29 | -------------------------------------------------------------------------------- /internal/core/storer/store_test.go: -------------------------------------------------------------------------------- 1 | package storer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/api7/apisix-seed/internal/core/message" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | ) 14 | 15 | func TestFromatKey(t *testing.T) { 16 | tests := []struct { 17 | caseDesc string 18 | giveKey string 19 | givePrefix string 20 | wantPrefix string 21 | wantEnity string 22 | wantID string 23 | }{ 24 | { 25 | caseDesc: "Normal case 1", 26 | giveKey: "/prefix/entity/1", 27 | givePrefix: "/prefix", 28 | wantPrefix: "/prefix", 29 | wantEnity: "entity", 30 | wantID: "1", 31 | }, 32 | { 33 | caseDesc: "Normal case 2", 34 | giveKey: "/prefix/entity/1/22", 35 | givePrefix: "/prefix", 36 | wantPrefix: "/prefix", 37 | wantEnity: "entity", 38 | wantID: "1/22", 39 | }, 40 | { 41 | caseDesc: "prefix not match", 42 | giveKey: "/prefix/entity/1/22", 43 | givePrefix: "/aaaa", 44 | wantPrefix: "", 45 | wantEnity: "", 46 | wantID: "", 47 | }, 48 | { 49 | caseDesc: "prefix equal key", 50 | giveKey: "/prefix/entity/1/22", 51 | givePrefix: "/prefix/entity/1/22", 52 | wantPrefix: "", 53 | wantEnity: "", 54 | wantID: "", 55 | }, 56 | { 57 | caseDesc: "prefix length is small than key", 58 | giveKey: "/prefix/entity/1/22", 59 | givePrefix: "/prefix/entity/1/22/dsadas", 60 | wantPrefix: "", 61 | wantEnity: "", 62 | wantID: "", 63 | }, 64 | { 65 | caseDesc: "key is invalid", 66 | giveKey: "/prefix//", 67 | givePrefix: "/prefix", 68 | wantPrefix: "/prefix", 69 | wantEnity: "", 70 | wantID: "", 71 | }, 72 | } 73 | 74 | for _, tc := range tests { 75 | prefix, entity, id := FromatKey(tc.giveKey, tc.givePrefix) 76 | assert.Equal(t, tc.wantPrefix, prefix, tc.caseDesc) 77 | assert.Equal(t, tc.wantEnity, entity) 78 | assert.Equal(t, tc.wantID, id) 79 | } 80 | } 81 | 82 | func TestNewGenericStore(t *testing.T) { 83 | tests := []struct { 84 | caseDesc string 85 | giveOpt GenericStoreOption 86 | wantStore *GenericStore 87 | wantErr error 88 | }{ 89 | { 90 | caseDesc: "Normal Case", 91 | giveOpt: GenericStoreOption{ 92 | BasePath: "test", 93 | }, 94 | wantStore: &GenericStore{ 95 | Stg: nil, 96 | opt: GenericStoreOption{ 97 | BasePath: "test", 98 | }, 99 | }, 100 | }, 101 | { 102 | caseDesc: "No BasePath", 103 | giveOpt: GenericStoreOption{ 104 | BasePath: "", 105 | }, 106 | wantErr: fmt.Errorf("base path can not be empty"), 107 | }, 108 | } 109 | 110 | for _, tc := range tests { 111 | s, err := NewGenericStore("test", tc.giveOpt, nil) 112 | assert.Equal(t, tc.wantErr, err, tc.caseDesc) 113 | if err != nil { 114 | continue 115 | } 116 | assert.Equal(t, tc.wantStore.Stg, s.Stg, tc.caseDesc) 117 | flag := reflect.DeepEqual(&tc.wantStore.cache, &s.cache) 118 | assert.True(t, flag, tc.caseDesc) 119 | assert.Equal(t, tc.wantStore.opt.BasePath, s.opt.BasePath, tc.caseDesc) 120 | } 121 | } 122 | 123 | func TestList(t *testing.T) { 124 | testMsgs := []struct { 125 | desc string 126 | key string 127 | a6Str string 128 | }{ 129 | { 130 | desc: "normal", 131 | key: "/apisxi/routes/1", 132 | a6Str: `{"uri":"/test","upstream":{"service_name":"APISIX-ZK","type":"roundrobin","discovery_type":"mock_zk"}}`, 133 | }, 134 | { 135 | desc: "routes with nodes", 136 | key: "/apisxi/routes/nodes", 137 | a6Str: `{"uri":"/test","upstream":{"nodes":{}}}`, 138 | }, 139 | } 140 | msgs := make([]*message.Message, 0, len(testMsgs)) 141 | for _, v := range testMsgs { 142 | msg, err := message.NewMessage(v.key, []byte(v.a6Str), 1, message.EventAdd, message.A6RoutesConf) 143 | assert.Nil(t, err, v.desc) 144 | msgs = append(msgs, msg) 145 | } 146 | mStg := &MockInterface{} 147 | mStg.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 148 | }).Return(msgs, nil) 149 | 150 | caseDesc := "list without filter" 151 | store, err := NewGenericStore("test", GenericStoreOption{ 152 | BasePath: "/apisix/routes", 153 | Prefix: "/apisix", 154 | }, mStg) 155 | assert.Nil(t, err, caseDesc) 156 | _, err = store.List(nil) 157 | assert.Nil(t, err, caseDesc) 158 | keys := make([]string, 0, len(testMsgs)) 159 | store.cache.Range(func(key, value interface{}) bool { 160 | keys = append(keys, key.(string)) 161 | return true 162 | }) 163 | assert.ElementsMatch(t, []string{"/apisxi/routes/1", "/apisxi/routes/nodes"}, keys) 164 | 165 | caseDesc = "list with filter" 166 | store, err = NewGenericStore("test", GenericStoreOption{ 167 | BasePath: "/apisix/routes", 168 | Prefix: "/apisix", 169 | }, mStg) 170 | assert.Nil(t, err, caseDesc) 171 | _, err = store.List(message.ServiceFilter) 172 | assert.Nil(t, err, caseDesc) 173 | store.cache.Range(func(key, value interface{}) bool { 174 | assert.Equal(t, "/apisxi/routes/1", key.(string), caseDesc) 175 | return true 176 | }) 177 | } 178 | 179 | func TestWatch(t *testing.T) { 180 | caseDesc := "sanity" 181 | ch := make(chan []*message.Message, 1) 182 | mStg := &MockInterface{} 183 | mStg.On("Watch", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 184 | }).Return(ch) 185 | store, err := NewGenericStore("test", GenericStoreOption{ 186 | BasePath: "/apisix/routes", 187 | Prefix: "/apisix", 188 | }, mStg) 189 | assert.Nil(t, err, caseDesc) 190 | 191 | a6Str := `{"uri":"/test","upstream":{"service_name":"APISIX-ZK","type":"roundrobin","discovery_type":"mock_zk"}}` 192 | givenMsg, err := message.NewMessage("/apisxi/routes/a", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 193 | assert.Nil(t, err, caseDesc) 194 | ch <- []*message.Message{givenMsg} 195 | 196 | msgs := <-store.Watch() 197 | assert.Equal(t, "/apisxi/routes/a", msgs[0].Key) 198 | } 199 | 200 | func TestUpdateNodes(t *testing.T) { 201 | caseDesc := "sanity" 202 | mStg := &MockInterface{} 203 | mStg.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 204 | }).Return(nil) 205 | store, err := NewGenericStore("test", GenericStoreOption{ 206 | BasePath: "/apisix/routes", 207 | Prefix: "/apisix", 208 | }, mStg) 209 | assert.Nil(t, err, caseDesc) 210 | 211 | a6Str := `{"uri":"/test","upstream":{"service_name":"APISIX-ZK","type":"roundrobin","discovery_type":"mock_zk"}}` 212 | givenMsg, err := message.NewMessage("/apisxi/routes/a", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 213 | assert.Nil(t, err, caseDesc) 214 | 215 | err = store.UpdateNodes(context.Background(), givenMsg) 216 | assert.Nil(t, err, caseDesc) 217 | } 218 | -------------------------------------------------------------------------------- /internal/core/storer/storehub.go: -------------------------------------------------------------------------------- 1 | package storer 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/api7/gopkg/pkg/log" 8 | 9 | "github.com/api7/apisix-seed/internal/conf" 10 | ) 11 | 12 | var storeHub = map[string]*GenericStore{} 13 | 14 | func InitStore(key string, opt GenericStoreOption, stg Interface) error { 15 | s, err := NewGenericStore(key, opt, stg) 16 | if err != nil { 17 | log.Errorf("New %s GenericStore err: %s", key, err) 18 | return err 19 | } 20 | 21 | storeHub[key] = s 22 | return nil 23 | } 24 | 25 | func InitStores(stg Interface) (err error) { 26 | err = InitStore("routes", GenericStoreOption{ 27 | BasePath: conf.ETCDConfig.Prefix + "/routes", 28 | Prefix: conf.ETCDConfig.Prefix, 29 | }, stg) 30 | if err != nil { 31 | return 32 | } 33 | 34 | err = InitStore("services", GenericStoreOption{ 35 | BasePath: conf.ETCDConfig.Prefix + "/services", 36 | Prefix: conf.ETCDConfig.Prefix, 37 | }, stg) 38 | if err != nil { 39 | return 40 | } 41 | 42 | err = InitStore("upstreams", GenericStoreOption{ 43 | BasePath: conf.ETCDConfig.Prefix + "/upstreams", 44 | Prefix: conf.ETCDConfig.Prefix, 45 | }, stg) 46 | if err != nil { 47 | return 48 | } 49 | 50 | return 51 | } 52 | 53 | func FromatKey(key, prefix string) (string, string, string) { 54 | s := strings.TrimPrefix(key, prefix) 55 | if s == "" || s == key { 56 | return "", "", "" 57 | } 58 | 59 | entityindecx := strings.IndexByte(s[1:], '/') 60 | if entityindecx == -1 { 61 | return prefix, "", "" 62 | } 63 | entity := s[1 : entityindecx+1] 64 | id := s[entityindecx+2:] 65 | 66 | return prefix, entity, id 67 | } 68 | 69 | func GetStore(entity string) *GenericStore { 70 | if s, ok := storeHub[entity]; ok { 71 | return s 72 | } 73 | panic(fmt.Sprintf("no store with key: %s", entity)) 74 | } 75 | 76 | func GetStores() []*GenericStore { 77 | stores := make([]*GenericStore, 0, len(storeHub)) 78 | for _, store := range storeHub { 79 | stores = append(stores, store) 80 | } 81 | return stores 82 | } 83 | 84 | func ClrearStores() { 85 | for key := range storeHub { 86 | delete(storeHub, key) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/discoverer/discoverer.go: -------------------------------------------------------------------------------- 1 | package discoverer 2 | 3 | import ( 4 | "github.com/api7/apisix-seed/internal/core/message" 5 | ) 6 | 7 | type NewDiscoverFunc func(disConfig interface{}) (Discoverer, error) 8 | 9 | // Discoverer defines the component that interact nacos, consul and so on 10 | type Discoverer interface { 11 | Stop() 12 | Query(*message.Message) error 13 | Update(*message.Message, *message.Message) error 14 | Delete(*message.Message) error 15 | Watch() chan *message.Message 16 | } 17 | -------------------------------------------------------------------------------- /internal/discoverer/discoverer_mock.go: -------------------------------------------------------------------------------- 1 | package discoverer 2 | 3 | import ( 4 | "github.com/api7/apisix-seed/internal/core/message" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type MockInterface struct { 9 | mock.Mock 10 | } 11 | 12 | func NewDiscovererMock(_ interface{}) (Discoverer, error) { 13 | return &MockInterface{}, nil 14 | } 15 | 16 | func (m *MockInterface) Stop() { 17 | _ = m.Called() 18 | } 19 | 20 | func (m *MockInterface) Query(msg *message.Message) error { 21 | ret := m.Called(msg) 22 | return ret.Error(0) 23 | } 24 | 25 | func (m *MockInterface) Update(oldMsg, msg *message.Message) error { 26 | ret := m.Called(oldMsg, msg) 27 | return ret.Error(0) 28 | } 29 | 30 | func (m *MockInterface) Delete(msg *message.Message) error { 31 | ret := m.Called(msg) 32 | return ret.Error(0) 33 | } 34 | 35 | func (m *MockInterface) Watch() chan *message.Message { 36 | ret := m.Called() 37 | return ret.Get(0).(chan *message.Message) 38 | } 39 | -------------------------------------------------------------------------------- /internal/discoverer/discovererhub.go: -------------------------------------------------------------------------------- 1 | package discoverer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/api7/gopkg/pkg/log" 7 | 8 | "github.com/api7/apisix-seed/internal/conf" 9 | ) 10 | 11 | var ( 12 | Discoveries = make(map[string]NewDiscoverFunc) 13 | ) 14 | 15 | var discovererHub = map[string]Discoverer{} 16 | 17 | func InitDiscoverer(key string, disConfig interface{}) error { 18 | discoverer, err := Discoveries[key](disConfig) 19 | if err != nil { 20 | log.Errorf("New %s Discoverer err: %s", key, err) 21 | return err 22 | } 23 | 24 | discovererHub[key] = discoverer 25 | return nil 26 | } 27 | 28 | func InitDiscoverers() (err error) { 29 | for key, disConfig := range conf.DisConfigs { 30 | err = InitDiscoverer(key, disConfig) 31 | if err != nil { 32 | return 33 | } 34 | } 35 | return 36 | } 37 | 38 | func GetDiscoverer(key string) Discoverer { 39 | if d, ok := discovererHub[key]; ok { 40 | return d 41 | } 42 | panic(fmt.Sprintf("no discoverer with key: %s", key)) 43 | } 44 | 45 | func GetDiscoverers() []Discoverer { 46 | discoverers := make([]Discoverer, 0, len(discovererHub)) 47 | for _, discoverer := range discovererHub { 48 | discoverers = append(discoverers, discoverer) 49 | } 50 | return discoverers 51 | } 52 | -------------------------------------------------------------------------------- /internal/discoverer/nacos.go: -------------------------------------------------------------------------------- 1 | package discoverer 2 | 3 | import ( 4 | "fmt" 5 | "hash" 6 | "hash/crc32" 7 | "net/url" 8 | "reflect" 9 | "strconv" 10 | "sync" 11 | 12 | "github.com/nacos-group/nacos-sdk-go/common/logger" 13 | 14 | "github.com/api7/apisix-seed/internal/conf" 15 | "github.com/api7/apisix-seed/internal/core/message" 16 | "github.com/api7/gopkg/pkg/log" 17 | "github.com/nacos-group/nacos-sdk-go/clients" 18 | "github.com/nacos-group/nacos-sdk-go/clients/naming_client" 19 | "github.com/nacos-group/nacos-sdk-go/common/constant" 20 | "github.com/nacos-group/nacos-sdk-go/model" 21 | "github.com/nacos-group/nacos-sdk-go/vo" 22 | ) 23 | 24 | func init() { 25 | Discoveries["nacos"] = NewNacosDiscoverer 26 | } 27 | 28 | func serviceID(service string, args map[string]interface{}) string { 29 | id, group := "", "" 30 | if args != nil { 31 | id, _ = args["namespace_id"].(string) 32 | group, _ = args["group_name"].(string) 33 | } 34 | serviceId := fmt.Sprintf("%s@%s@%s", id, group, service) 35 | return serviceId 36 | } 37 | 38 | type NacosService struct { 39 | id string 40 | name string 41 | args map[string]interface{} 42 | nodes []*message.Node // nodes are the upstream machines of the service 43 | a6Conf map[string]*message.Message // entities are the upstreams/services/routes that use the service 44 | } 45 | 46 | type NacosDiscoverer struct { 47 | timeout uint64 48 | weight int 49 | // nacos server configs, grouping by authentication information 50 | ServerConfigs map[string][]constant.ServerConfig 51 | // nacos naming clients, grouping by authentication information 52 | namingClients map[string][]naming_client.INamingClient 53 | // nacos username 54 | serverUser string 55 | // nacos password 56 | serverPassword string 57 | 58 | paramMutex sync.Mutex 59 | params map[string]*vo.SubscribeParam 60 | cacheMutex sync.Mutex 61 | cache map[string]*NacosService 62 | 63 | crc hash.Hash32 64 | 65 | msgCh chan *message.Message 66 | } 67 | 68 | func NewNacosDiscoverer(disConfig interface{}) (Discoverer, error) { 69 | config := disConfig.(*conf.Nacos) 70 | 71 | serverConfigs := make(map[string][]constant.ServerConfig) 72 | for _, host := range config.Host { 73 | u, err := url.Parse(host) 74 | if err != nil { 75 | log.Errorf("parse url fail: %s", err) 76 | return nil, err 77 | } 78 | 79 | port := 8848 // nacos default port 80 | if portStr := u.Port(); len(portStr) != 0 { 81 | port, _ = strconv.Atoi(portStr) 82 | } 83 | 84 | auth := config.User 85 | serverConfigs[auth] = append(serverConfigs[auth], constant.ServerConfig{ 86 | IpAddr: u.Hostname(), 87 | Port: uint64(port), 88 | Scheme: u.Scheme, 89 | ContextPath: config.Prefix, 90 | }) 91 | } 92 | 93 | timeout := config.Timeout 94 | discoverer := NacosDiscoverer{ 95 | // compatible with past timeout configurations 96 | timeout: uint64(timeout.Connect + timeout.Read + timeout.Send), 97 | weight: config.Weight, 98 | ServerConfigs: serverConfigs, 99 | serverUser: config.User, 100 | serverPassword: config.Password, 101 | namingClients: make(map[string][]naming_client.INamingClient), 102 | paramMutex: sync.Mutex{}, 103 | params: make(map[string]*vo.SubscribeParam), 104 | cacheMutex: sync.Mutex{}, 105 | cache: make(map[string]*NacosService), 106 | crc: crc32.NewIEEE(), 107 | msgCh: make(chan *message.Message, 10), 108 | } 109 | logger.SetLogger(log.DefaultLogger) 110 | return &discoverer, nil 111 | } 112 | 113 | func (d *NacosDiscoverer) Stop() { 114 | d.cacheMutex.Lock() 115 | defer d.cacheMutex.Unlock() 116 | 117 | close(d.msgCh) 118 | 119 | // Unsubscribe all services 120 | for _, service := range d.cache { 121 | d.unsubscribe(service) 122 | } 123 | } 124 | 125 | func (d *NacosDiscoverer) Query(msg *message.Message) error { 126 | serviceId := serviceID(msg.ServiceName(), msg.DiscoveryArgs()) 127 | 128 | d.cacheMutex.Lock() 129 | defer d.cacheMutex.Unlock() 130 | 131 | if discover, ok := d.cache[serviceId]; ok { 132 | // cache information is already available 133 | msg.InjectNodes(discover.nodes) 134 | discover.a6Conf[msg.Key] = msg 135 | } else { 136 | // fetch new service information 137 | dis := &NacosService{ 138 | id: serviceId, 139 | name: msg.ServiceName(), 140 | args: msg.DiscoveryArgs(), 141 | } 142 | nodes, err := d.fetch(dis) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | msg.InjectNodes(nodes) 148 | 149 | dis.nodes = nodes 150 | dis.a6Conf = map[string]*message.Message{ 151 | msg.Key: msg, 152 | } 153 | 154 | d.cache[serviceId] = dis 155 | } 156 | d.msgCh <- msg 157 | 158 | return nil 159 | } 160 | 161 | func (d *NacosDiscoverer) Delete(msg *message.Message) error { 162 | serviceId := serviceID(msg.ServiceName(), msg.DiscoveryArgs()) 163 | 164 | d.cacheMutex.Lock() 165 | defer d.cacheMutex.Unlock() 166 | 167 | if discover, ok := d.cache[serviceId]; ok { 168 | delete(discover.a6Conf, msg.Key) 169 | 170 | // When a service is not used, it needs to be unsubscribed 171 | if len(discover.a6Conf) == 0 { 172 | d.unsubscribe(discover) 173 | delete(d.cache, serviceId) 174 | } 175 | } 176 | return nil 177 | } 178 | 179 | func (d *NacosDiscoverer) Update(oldMsg, msg *message.Message) error { 180 | msgArgs := oldMsg.DiscoveryArgs() 181 | newMsgArgs := msg.DiscoveryArgs() 182 | serviceId := serviceID(oldMsg.ServiceName(), msgArgs) 183 | newServiceId := serviceID(msg.ServiceName(), newMsgArgs) 184 | 185 | d.cacheMutex.Lock() 186 | defer d.cacheMutex.Unlock() 187 | if discover, ok := d.cache[serviceId]; ok { 188 | if serviceId == newServiceId && reflect.DeepEqual(msgArgs["metadata"], newMsgArgs["metadata"]) { 189 | discover.a6Conf[msg.Key].Version = msg.Version 190 | return nil 191 | } 192 | 193 | d.unsubscribe(discover) 194 | 195 | newDiscover := &NacosService{ 196 | args: msg.DiscoveryArgs(), 197 | id: newServiceId, 198 | name: msg.ServiceName(), 199 | } 200 | nodes, err := d.fetch(newDiscover) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | msg.InjectNodes(nodes) 206 | newDiscover.nodes = nodes 207 | newDiscover.a6Conf = map[string]*message.Message{ 208 | msg.Key: msg, 209 | } 210 | 211 | delete(d.cache, serviceId) 212 | d.cache[newServiceId] = newDiscover 213 | 214 | d.msgCh <- msg 215 | } 216 | 217 | return nil 218 | } 219 | 220 | func (d *NacosDiscoverer) Watch() chan *message.Message { 221 | return d.msgCh 222 | } 223 | 224 | func (d *NacosDiscoverer) fetch(service *NacosService) ([]*message.Node, error) { 225 | // if the namespace client has not yet been created 226 | namespace, _ := service.args["namespace_id"].(string) 227 | if _, ok := d.namingClients[namespace]; !ok { 228 | err := d.newClient(namespace) 229 | if err != nil { 230 | return nil, err 231 | } 232 | } 233 | 234 | client := d.namingClients[namespace][d.hash(service.id, namespace)] 235 | 236 | groupName, _ := service.args["group_name"].(string) 237 | serviceInfo, err := client.GetService(vo.GetServiceParam{ 238 | ServiceName: service.name, 239 | GroupName: groupName, 240 | }) 241 | if err != nil { 242 | log.Errorf("Nacos get service[%s] error: %s", service.name, err) 243 | return nil, err 244 | } 245 | 246 | // watch the new service 247 | if err = d.subscribe(service, client); err != nil { 248 | log.Errorf("Nacos subscribe service[%s] error: %s", service.name, err) 249 | return nil, err 250 | } 251 | 252 | // metadata 253 | metadata := service.args["metadata"] 254 | nodes := make([]*message.Node, 0) 255 | for _, host := range serviceInfo.Hosts { 256 | if metadata != nil { 257 | discard := 0 258 | for k, v := range metadata.(map[string]interface{}) { 259 | if host.Metadata[k] != v { 260 | discard = 1 261 | } 262 | } 263 | if discard == 1 { 264 | continue 265 | } 266 | } 267 | 268 | weight := int(host.Weight) 269 | if weight == 0 { 270 | weight = d.weight 271 | } 272 | 273 | nodes = append(nodes, &message.Node{ 274 | Host: host.Ip, 275 | Port: int(host.Port), 276 | Weight: weight, 277 | }) 278 | } 279 | 280 | return nodes, nil 281 | } 282 | 283 | func (d *NacosDiscoverer) newSubscribeCallback(serviceId string, metadata interface{}) func([]model.SubscribeService, error) { 284 | return func(services []model.SubscribeService, err error) { 285 | nodes := make([]*message.Node, 0) 286 | meta, ok := metadata.(map[string]interface{}) 287 | 288 | for _, inst := range services { 289 | if ok { 290 | discard := 0 291 | for k, v := range meta { 292 | if inst.Metadata[k] != v { 293 | discard = 1 294 | } 295 | } 296 | if discard == 1 { 297 | continue 298 | } 299 | } 300 | 301 | weight := int(inst.Weight) 302 | if weight == 0 { 303 | weight = d.weight 304 | } 305 | 306 | nodes = append(nodes, &message.Node{ 307 | Host: inst.Ip, 308 | Port: int(inst.Port), 309 | Weight: weight, 310 | }) 311 | } 312 | 313 | d.cacheMutex.Lock() 314 | defer d.cacheMutex.Unlock() 315 | 316 | discover := d.cache[serviceId] 317 | discover.nodes = nodes 318 | 319 | for _, msg := range discover.a6Conf { 320 | msg.InjectNodes(nodes) 321 | d.msgCh <- msg 322 | } 323 | } 324 | } 325 | 326 | func (d *NacosDiscoverer) subscribe(service *NacosService, client naming_client.INamingClient) error { 327 | groupName, _ := service.args["group_name"].(string) 328 | log.Infof("Nacos subscribe service: %s, groupName: %s", service.name, groupName) 329 | 330 | param := &vo.SubscribeParam{ 331 | ServiceName: service.name, 332 | GroupName: groupName, 333 | SubscribeCallback: d.newSubscribeCallback(service.id, service.args["metadata"]), 334 | } 335 | 336 | // TODO: retry if failed to Subscribe 337 | err := client.Subscribe(param) 338 | if err == nil { 339 | d.paramMutex.Lock() 340 | d.params[service.id] = param 341 | d.paramMutex.Unlock() 342 | } 343 | return err 344 | } 345 | 346 | func (d *NacosDiscoverer) unsubscribe(service *NacosService) { 347 | log.Infof("Nacos unsubscribe service %s", service.name) 348 | param := d.params[service.id] 349 | 350 | namespace, _ := service.args["namespace_id"].(string) 351 | client := d.namingClients[namespace][d.hash(service.id, namespace)] 352 | 353 | // the nacos unsubscribe function returns only nil 354 | // so ignore the error handling 355 | _ = client.Unsubscribe(param) 356 | delete(d.params, service.id) 357 | } 358 | 359 | func (d *NacosDiscoverer) newClient(namespace string) error { 360 | newClients := make([]naming_client.INamingClient, 0, len(d.ServerConfigs)) 361 | username := d.serverUser 362 | password := d.serverPassword 363 | for _, serverConfigs := range d.ServerConfigs { 364 | 365 | clientConfig := constant.ClientConfig{ 366 | TimeoutMs: d.timeout, 367 | NamespaceId: namespace, 368 | Username: username, 369 | Password: password, 370 | NotLoadCacheAtStart: true, 371 | UpdateCacheWhenEmpty: true, 372 | } 373 | client, err := clients.NewNamingClient( 374 | vo.NacosClientParam{ 375 | ClientConfig: &clientConfig, 376 | ServerConfigs: serverConfigs, 377 | }, 378 | ) 379 | if err != nil { 380 | log.Errorf("Nacos new client error: %s", err) 381 | return err 382 | } 383 | newClients = append(newClients, client) 384 | } 385 | 386 | d.namingClients[namespace] = newClients 387 | // logger.InitLogger will be called, we should SetLogger again 388 | logger.SetLogger(log.DefaultLogger) 389 | log.Info("Successfully create a new Nacos client") 390 | return nil 391 | } 392 | 393 | // hash distributes the serviceId to different clients using CRC32 394 | func (d *NacosDiscoverer) hash(serviceId, namespace string) int { 395 | d.crc.Reset() 396 | _, _ = d.crc.Write([]byte(serviceId)) 397 | return int(d.crc.Sum32()) % len(d.namingClients[namespace]) 398 | } 399 | -------------------------------------------------------------------------------- /internal/discoverer/nacos_test.go: -------------------------------------------------------------------------------- 1 | package discoverer 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/url" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/api7/apisix-seed/internal/core/message" 12 | "gopkg.in/yaml.v3" 13 | 14 | "github.com/api7/apisix-seed/internal/conf" 15 | "github.com/nacos-group/nacos-sdk-go/clients" 16 | "github.com/nacos-group/nacos-sdk-go/common/constant" 17 | "github.com/nacos-group/nacos-sdk-go/vo" 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | var naYamlConfig = ` 22 | host: 23 | - "http://127.0.0.1:8848" 24 | prefix: ~ 25 | ` 26 | 27 | var naYamlConfigWithPasswd = ` 28 | host: 29 | - "https://console.nacos.io:8858" 30 | user: "username" 31 | password: "password" 32 | ` 33 | 34 | func getNaConfig(str string) (*conf.Nacos, error) { 35 | naConf := &conf.Nacos{} 36 | err := yaml.Unmarshal([]byte(str), naConf) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return naConf, nil 41 | } 42 | 43 | var TestService string 44 | 45 | func init() { 46 | rand.Seed(time.Now().UnixNano()) 47 | TestService = fmt.Sprintf("APISIX-SEED-TEST-%d", rand.Int()) 48 | } 49 | 50 | func TestServerConfig(t *testing.T) { 51 | nacosConf, err := getNaConfig(naYamlConfigWithPasswd) 52 | assert.Nil(t, err) 53 | discoverer, err := NewNacosDiscoverer(nacosConf) 54 | assert.Nil(t, err) 55 | nacosDiscoverer := discoverer.(*NacosDiscoverer) 56 | 57 | for auth, serverConfigs := range nacosDiscoverer.ServerConfigs { 58 | assert.True(t, auth == "username", "Test auth") 59 | assert.Len(t, serverConfigs, 1) 60 | 61 | config := serverConfigs[0] 62 | assert.True(t, config.Scheme == "https", "Test scheme") 63 | assert.True(t, config.Port == 8858, "Test port") 64 | } 65 | 66 | err = nacosDiscoverer.newClient("APISIX") 67 | assert.Nil(t, err) 68 | } 69 | 70 | func TestNacosDiscoverer(t *testing.T) { 71 | nacosConf, err := getNaConfig(naYamlConfig) 72 | assert.Nil(t, err) 73 | 74 | discoverer, err := NewNacosDiscoverer(nacosConf) 75 | assert.Nil(t, err) 76 | 77 | testQueryService(t, discoverer) 78 | testUpdateArgs(t, discoverer) 79 | testUpdateUnmatchedMetadata(t, discoverer) 80 | testUpdateMatchedMetadata(t, discoverer) 81 | testOnlyUpdateMetadata(t, discoverer) 82 | testDeleteService(t, discoverer) 83 | } 84 | 85 | func testQueryService(t *testing.T, discoverer Discoverer) { 86 | registerService(t, "10.0.0.11", "", map[string]string{"idc": "shanghai"}) 87 | 88 | a6fmt := `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"%s"}}` 89 | a6Str := fmt.Sprintf(a6fmt, TestService) 90 | expectA6StrFmt := `{ 91 | "uri": "/hh", 92 | "upstream": { 93 | "nodes": [ 94 | {"host":"%s","port": %d,"weight":%d} 95 | ], 96 | "_discovery_type":"nacos","_service_name":"%s"}}` 97 | msg, err := message.NewMessage("/apisix/routes/1", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 98 | assert.Nil(t, err) 99 | tests := []struct { 100 | caseDesc string 101 | givenMsg *message.Message 102 | wantA6Str string 103 | }{ 104 | { 105 | caseDesc: "Test query new service", 106 | givenMsg: msg, 107 | wantA6Str: fmt.Sprintf(expectA6StrFmt, "10.0.0.11", 8848, 10, TestService), 108 | }, 109 | { 110 | caseDesc: "Test query new service", 111 | givenMsg: msg, 112 | wantA6Str: fmt.Sprintf(expectA6StrFmt, "10.0.0.11", 8848, 10, TestService), 113 | }, 114 | } 115 | 116 | for _, tc := range tests { 117 | err = discoverer.Query(tc.givenMsg) 118 | assert.Nil(t, err) 119 | watchMsg := <-discoverer.Watch() 120 | assert.JSONEq(t, tc.wantA6Str, naMsg2Value(watchMsg), tc.caseDesc) 121 | } 122 | } 123 | 124 | func cacheMsg(t *testing.T, discoverer Discoverer, msg *message.Message) { 125 | err := discoverer.Query(msg) 126 | assert.Nil(t, err) 127 | <-discoverer.Watch() 128 | } 129 | 130 | func testUpdateArgs(t *testing.T, discoverer Discoverer) { 131 | TestGroup := fmt.Sprintf("Group-%d", rand.Int()) 132 | registerService(t, "10.0.0.13", TestGroup, map[string]string{"idc": "shanghai"}) 133 | 134 | caseDesc := "Test update service args" 135 | oldA6StrFmt := `{ 136 | "uri": "/hh", 137 | "upstream": { 138 | "nodes": [ 139 | {"host": "%s","port": %d,"weight":%d} 140 | ], 141 | "_discovery_type":"nacos","_service_name":"%s"}}` 142 | oldA6Str := fmt.Sprintf(oldA6StrFmt, "10.0.0.11", 8848, 10, TestService) 143 | 144 | oldMsg, err := message.NewMessage("/apisix/routes/1", []byte(oldA6Str), 1, message.EventAdd, message.A6RoutesConf) 145 | assert.Nil(t, err) 146 | cacheMsg(t, discoverer, oldMsg) 147 | 148 | a6fmt := `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"%s","discovery_args":{"group_name":"%s"}}}` 149 | a6Str := fmt.Sprintf(a6fmt, TestService, TestGroup) 150 | msg, err := message.NewMessage("/apisix/routes/1", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 151 | assert.Nil(t, err) 152 | err = discoverer.Update(oldMsg, msg) 153 | assert.Nil(t, err, caseDesc) 154 | 155 | expectA6StrFmt := `{ 156 | "uri": "/hh", 157 | "upstream": { 158 | "nodes": [ 159 | {"host": "%s","port": %d,"weight":%d} 160 | ], 161 | "_discovery_type":"nacos","_service_name":"%s","discovery_args":{"group_name":"%s"}}}` 162 | expectA6Str := fmt.Sprintf(expectA6StrFmt, "10.0.0.13", 8848, 10, TestService, TestGroup) 163 | 164 | watchMsg := <-discoverer.Watch() 165 | assert.JSONEq(t, expectA6Str, naMsg2Value(watchMsg), caseDesc) 166 | 167 | // If use the wrong sericeId to cache, register a new instance will raise a panic 168 | registerService(t, "10.0.0.14", TestGroup, map[string]string{"idc": "shanghai"}) 169 | <-discoverer.Watch() 170 | 171 | _ = discoverer.Delete(msg) 172 | } 173 | 174 | func testUpdateUnmatchedMetadata(t *testing.T, discoverer Discoverer) { 175 | TestGroup := fmt.Sprintf("Group-%d", rand.Int()) 176 | registerService(t, "10.0.0.15", TestGroup, map[string]string{"idc": "shanghai"}) 177 | 178 | caseDesc := "Test update service args" 179 | oldA6StrFmt := `{ 180 | "uri": "/hh", 181 | "upstream": { 182 | "nodes": [ 183 | {"host": "%s","port": %d,"weight":%d} 184 | ], 185 | "_discovery_type":"nacos","_service_name":"%s"}}` 186 | oldA6Str := fmt.Sprintf(oldA6StrFmt, "10.0.0.11", 8848, 10, TestService) 187 | oldMsg, err := message.NewMessage("/apisix/routes/1", []byte(oldA6Str), 1, message.EventAdd, message.A6RoutesConf) 188 | assert.Nil(t, err) 189 | cacheMsg(t, discoverer, oldMsg) 190 | 191 | //unmatched metadata 192 | a6fmt := `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"%s","discovery_args":{"group_name":"%s","metadata":{"version":"v1"}}}}` 193 | a6Str := fmt.Sprintf(a6fmt, TestService, TestGroup) 194 | msg, err := message.NewMessage("/apisix/routes/1", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 195 | assert.Nil(t, err) 196 | err = discoverer.Update(oldMsg, msg) 197 | assert.Nil(t, err, caseDesc) 198 | 199 | expectA6StrFmt := `{ 200 | "uri": "/hh", 201 | "upstream": { 202 | "nodes": [], 203 | "_discovery_type":"nacos","_service_name":"%s","discovery_args":{"group_name":"%s","metadata":{"version":"v1"}}}}` 204 | expectA6Str := fmt.Sprintf(expectA6StrFmt, TestService, TestGroup) 205 | 206 | watchMsg := <-discoverer.Watch() 207 | assert.JSONEq(t, expectA6Str, naMsg2Value(watchMsg), caseDesc) 208 | 209 | _ = discoverer.Delete(msg) 210 | } 211 | 212 | func testUpdateMatchedMetadata(t *testing.T, discoverer Discoverer) { 213 | TestGroup := fmt.Sprintf("Group-%d", rand.Int()) 214 | registerService(t, "10.0.0.16", TestGroup, map[string]string{"version": "v1"}) 215 | 216 | caseDesc := "Test update service args" 217 | oldA6StrFmt := `{ 218 | "uri": "/hh", 219 | "upstream": { 220 | "nodes": [ 221 | {"host": "%s","port": %d,"weight":%d} 222 | ], 223 | "_discovery_type":"nacos","_service_name":"%s"}}` 224 | oldA6Str := fmt.Sprintf(oldA6StrFmt, "10.0.0.11", 8848, 10, TestService) 225 | oldMsg, err := message.NewMessage("/apisix/routes/1", []byte(oldA6Str), 1, message.EventAdd, message.A6RoutesConf) 226 | assert.Nil(t, err) 227 | cacheMsg(t, discoverer, oldMsg) 228 | 229 | // matched metadata 230 | a6fmt := `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"%s","discovery_args":{"group_name":"%s","metadata":{"version":"v1"}}}}` 231 | a6Str := fmt.Sprintf(a6fmt, TestService, TestGroup) 232 | msg, err := message.NewMessage("/apisix/routes/1", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 233 | assert.Nil(t, err) 234 | err = discoverer.Update(oldMsg, msg) 235 | assert.Nil(t, err, caseDesc) 236 | 237 | expectA6StrFmt := `{ 238 | "uri": "/hh", 239 | "upstream": { 240 | "nodes": [ 241 | {"host": "%s","port": %d,"weight":%d} 242 | ], 243 | "_discovery_type":"nacos","_service_name":"%s","discovery_args":{"group_name":"%s","metadata":{"version":"v1"}}}}` 244 | expectA6Str := fmt.Sprintf(expectA6StrFmt, "10.0.0.16", 8848, 10, TestService, TestGroup) 245 | 246 | watchMsg := <-discoverer.Watch() 247 | assert.JSONEq(t, expectA6Str, naMsg2Value(watchMsg), caseDesc) 248 | 249 | _ = discoverer.Delete(msg) 250 | } 251 | 252 | func testOnlyUpdateMetadata(t *testing.T, discoverer Discoverer) { 253 | TestGroup := fmt.Sprintf("Group-%d", rand.Int()) 254 | registerService(t, "10.0.0.17", TestGroup, map[string]string{"version": "v2"}) 255 | 256 | caseDesc := "Test update service args" 257 | oldA6StrFmt := `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"%s","discovery_args":{"group_name":"%s","metadata":{"version":"v1"}}}}` 258 | oldA6Str := fmt.Sprintf(oldA6StrFmt, TestService, TestGroup) 259 | oldMsg, err := message.NewMessage("/apisix/routes/1", []byte(oldA6Str), 1, message.EventAdd, message.A6RoutesConf) 260 | assert.Nil(t, err) 261 | cacheMsg(t, discoverer, oldMsg) 262 | 263 | // matched metadata 264 | a6fmt := `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"%s","discovery_args":{"group_name":"%s","metadata":{"version":"v2"}}}}` 265 | a6Str := fmt.Sprintf(a6fmt, TestService, TestGroup) 266 | msg, err := message.NewMessage("/apisix/routes/1", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 267 | assert.Nil(t, err) 268 | err = discoverer.Update(oldMsg, msg) 269 | assert.Nil(t, err, caseDesc) 270 | 271 | expectA6StrFmt := `{ 272 | "uri": "/hh", 273 | "upstream": { 274 | "nodes": [ 275 | {"host": "%s","port": %d,"weight":%d} 276 | ], 277 | "_discovery_type":"nacos","_service_name":"%s","discovery_args":{"group_name":"%s","metadata":{"version":"v2"}}}}` 278 | expectA6Str := fmt.Sprintf(expectA6StrFmt, "10.0.0.17", 8848, 10, TestService, TestGroup) 279 | 280 | watchMsg := <-discoverer.Watch() 281 | assert.JSONEq(t, expectA6Str, naMsg2Value(watchMsg), caseDesc) 282 | 283 | _ = discoverer.Delete(msg) 284 | } 285 | 286 | func testDeleteService(t *testing.T, discoverer Discoverer) { 287 | caseDesc := "Test delete service" 288 | // First delete the service 289 | a6fmt := `{"uri":"/hh","upstream":{"discovery_type":"nacos","service_name":"%s"}}` 290 | a6Str := fmt.Sprintf(a6fmt, TestService) 291 | msg, err := message.NewMessage("/apisix/routes/1", []byte(a6Str), 1, message.EventAdd, message.A6RoutesConf) 292 | assert.Nil(t, err) 293 | err = discoverer.Delete(msg) 294 | assert.Nil(t, err) 295 | 296 | registerService(t, "10.0.0.18", "", map[string]string{"idc": "shanghai"}) 297 | select { 298 | case <-discoverer.Watch(): 299 | // Since the subscription is cancelled, the receiving operation will be blocked 300 | assert.True(t, false, caseDesc) 301 | case <-time.After(3 * time.Second): 302 | } 303 | } 304 | 305 | func registerService(t *testing.T, ip string, group string, metadata map[string]string) { 306 | conf, err := getNaConfig(naYamlConfig) 307 | assert.Nil(t, err) 308 | serverConfigs := make([]constant.ServerConfig, 0, len(conf.Host)) 309 | for _, host := range conf.Host { 310 | u, _ := url.Parse(host) 311 | port := 8848 // nacos default port 312 | if portStr := u.Port(); len(portStr) != 0 { 313 | port, _ = strconv.Atoi(portStr) 314 | } 315 | serverConfig := *constant.NewServerConfig( 316 | u.Hostname(), 317 | uint64(port), 318 | constant.WithScheme(u.Scheme), 319 | constant.WithContextPath(conf.Prefix), 320 | ) 321 | serverConfigs = append(serverConfigs, serverConfig) 322 | } 323 | //Another way of create clientConfig 324 | clientConfig := constant.NewClientConfig( 325 | constant.WithTimeoutMs(5000), 326 | constant.WithNotLoadCacheAtStart(true), 327 | constant.WithLogLevel("info"), 328 | ) 329 | 330 | // For register some services to test 331 | registerClient, _ := clients.NewNamingClient( 332 | vo.NacosClientParam{ 333 | ClientConfig: clientConfig, 334 | ServerConfigs: serverConfigs, 335 | }, 336 | ) 337 | 338 | success, err := registerClient.RegisterInstance(vo.RegisterInstanceParam{ 339 | Ip: ip, 340 | Port: 8848, 341 | ServiceName: TestService, 342 | GroupName: group, 343 | Weight: 10, 344 | Enable: true, 345 | Healthy: true, 346 | Metadata: metadata, 347 | }) 348 | assert.NoError(t, err) 349 | assert.True(t, success) 350 | } 351 | 352 | func naMsg2Value(msg *message.Message) string { 353 | str, _ := msg.Marshal() 354 | return string(str) 355 | } 356 | -------------------------------------------------------------------------------- /internal/discoverer/zookeeper.go: -------------------------------------------------------------------------------- 1 | package discoverer 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "sync" 7 | "time" 8 | 9 | "github.com/api7/gopkg/pkg/log" 10 | 11 | "github.com/api7/apisix-seed/internal/core/message" 12 | 13 | "github.com/api7/apisix-seed/internal/conf" 14 | "github.com/go-zookeeper/zk" 15 | "golang.org/x/net/context" 16 | ) 17 | 18 | func init() { 19 | Discoveries["zookeeper"] = NewZookeeperDiscoverer 20 | } 21 | 22 | type ZookeeperService struct { 23 | Name string 24 | mutex *sync.Mutex 25 | BindEntities map[string]*message.Message 26 | WatchPath string 27 | WatchContext context.Context 28 | WatchCancel context.CancelFunc 29 | } 30 | 31 | type ZookeeperDiscoverer struct { 32 | zkConfig *conf.Zookeeper 33 | zkConn *zk.Conn 34 | zkWatchServices sync.Map 35 | zkUnWatchServices sync.Map 36 | zkUnWatchContext context.Context 37 | zkUnWatchCancel context.CancelFunc 38 | 39 | msgCh chan *message.Message 40 | } 41 | 42 | func (zd *ZookeeperDiscoverer) Stop() { 43 | zd.zkWatchServices.Range(func(key, value interface{}) bool { 44 | zd.removeWatchService(value.(*ZookeeperService)) 45 | return true 46 | }) 47 | close(zd.msgCh) 48 | zd.zkConn.Close() 49 | zd.zkUnWatchCancel() 50 | } 51 | 52 | func (zd *ZookeeperDiscoverer) Query(msg *message.Message) error { 53 | return zd.fetchService(msg.ServiceName(), map[string]*message.Message{msg.Key: msg}) 54 | } 55 | 56 | func (zd *ZookeeperDiscoverer) Update(oldMsg, msg *message.Message) error { 57 | zkService, ok := zd.zkWatchServices.Load(oldMsg.ServiceName()) 58 | if !ok { 59 | return nil 60 | } 61 | service := zkService.(*ZookeeperService) 62 | service.mutex.Lock() 63 | defer service.mutex.Unlock() 64 | if _, ok = service.BindEntities[oldMsg.Key]; ok { 65 | service.BindEntities[oldMsg.Key].Version = msg.Version 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (zd *ZookeeperDiscoverer) Delete(msg *message.Message) error { 72 | return zd.removeService(msg.ServiceName(), false) 73 | } 74 | 75 | func (zd *ZookeeperDiscoverer) Watch() chan *message.Message { 76 | return zd.msgCh 77 | } 78 | 79 | // fetchService fetch service watch and send message notify 80 | func (zd *ZookeeperDiscoverer) fetchService(serviceName string, a6conf map[string]*message.Message) error { 81 | var service *ZookeeperService 82 | zkService, ok := zd.zkWatchServices.Load(serviceName) 83 | 84 | if ok { 85 | service = zkService.(*ZookeeperService) 86 | } else { 87 | var err error 88 | service, err = zd.newZookeeperClient(serviceName) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | zd.addWatchService(service) 94 | } 95 | 96 | service.mutex.Lock() 97 | for k, msg := range a6conf { 98 | if _, ok = service.BindEntities[k]; !ok { 99 | service.BindEntities[k] = msg 100 | } else { 101 | service.BindEntities[k].Version = msg.Version 102 | } 103 | } 104 | service.mutex.Unlock() 105 | 106 | serviceInfo, _, err := zd.zkConn.Get(service.WatchPath) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | var nodes []*message.Node 112 | err = json.Unmarshal(serviceInfo, &nodes) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | zd.sendMessage(service, nodes) 118 | 119 | return nil 120 | } 121 | 122 | // removeService remove service watch and send message notify 123 | func (zd *ZookeeperDiscoverer) removeService(serviceName string, isRewrite bool) error { 124 | zkService, ok := zd.zkWatchServices.Load(serviceName) 125 | if !ok { 126 | return errors.New("Zookeeper service: " + serviceName + " undefined") 127 | } 128 | 129 | if isRewrite { 130 | zd.sendMessage(zkService.(*ZookeeperService), make([]*message.Node, 0)) 131 | } 132 | 133 | zd.removeWatchService(zkService.(*ZookeeperService)) 134 | 135 | return nil 136 | } 137 | 138 | // sendMessage send message notify 139 | func (zd *ZookeeperDiscoverer) sendMessage(zkService *ZookeeperService, nodes []*message.Node) { 140 | for _, msg := range zkService.BindEntities { 141 | msg.InjectNodes(nodes) 142 | zd.msgCh <- msg 143 | } 144 | } 145 | 146 | // NewZookeeperDiscoverer generate zookeeper discoverer instance 147 | func NewZookeeperDiscoverer(disConfig interface{}) (Discoverer, error) { 148 | config := disConfig.(*conf.Zookeeper) 149 | 150 | conn, _, err := zk.Connect(config.Hosts, time.Second*time.Duration(config.Timeout)) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | ctx, cancel := context.WithCancel(context.Background()) 156 | discoverer := ZookeeperDiscoverer{ 157 | msgCh: make(chan *message.Message, 10), 158 | zkConfig: config, 159 | zkConn: conn, 160 | zkWatchServices: sync.Map{}, 161 | zkUnWatchServices: sync.Map{}, 162 | zkUnWatchContext: ctx, 163 | zkUnWatchCancel: cancel, 164 | } 165 | 166 | err = discoverer.initZookeeperRoot() 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | go discoverer.watchServicePrefix() 172 | 173 | return &discoverer, nil 174 | } 175 | 176 | // initZookeeperRoot generate zookeeper root path 177 | func (zd *ZookeeperDiscoverer) initZookeeperRoot() error { 178 | ok, _, err := zd.zkConn.Exists(zd.zkConfig.Prefix) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | if !ok { 184 | _, err = zd.zkConn.Create(zd.zkConfig.Prefix, []byte(""), 0, zk.WorldACL(zk.PermAll)) 185 | if err != nil { 186 | return err 187 | } 188 | } 189 | 190 | return nil 191 | } 192 | 193 | // newZookeeperClient generate zookeeper client 194 | func (zd *ZookeeperDiscoverer) newZookeeperClient(serviceName string) (*ZookeeperService, error) { 195 | ctx, cancel := context.WithCancel(context.Background()) 196 | watchPath := zd.zkConfig.Prefix + "/" + serviceName 197 | service := &ZookeeperService{ 198 | Name: serviceName, 199 | mutex: &sync.Mutex{}, 200 | BindEntities: make(map[string]*message.Message), 201 | WatchPath: watchPath, 202 | WatchContext: ctx, 203 | WatchCancel: cancel, 204 | } 205 | 206 | return service, nil 207 | } 208 | 209 | // watchServicePrefix watch service prefix change, update unwatch service 210 | func (zd *ZookeeperDiscoverer) watchServicePrefix() { 211 | for { 212 | _, _, event, err := zd.zkConn.ChildrenW(zd.zkConfig.Prefix) 213 | if err != nil { 214 | log.Errorf("watch service prefix: %s fail, err: %s", zd.zkConfig.Prefix, err) 215 | return 216 | } 217 | 218 | select { 219 | case <-zd.zkUnWatchContext.Done(): 220 | return 221 | case <-event: 222 | var serviceNames []string 223 | serviceNames, _, err = zd.zkConn.Children(zd.zkConfig.Prefix) 224 | if err != nil { 225 | log.Errorf("fetch service prefix: %s fail, err: %s", zd.zkConfig.Prefix, err) 226 | continue 227 | } 228 | 229 | for _, serviceName := range serviceNames { 230 | a6Entity, ok := zd.zkUnWatchServices.Load(serviceName) 231 | if ok { 232 | err = zd.fetchService(serviceName, a6Entity.(map[string]*message.Message)) 233 | if err != nil { 234 | log.Errorf("fetch service: %s fail, err: %s", serviceName, err) 235 | } 236 | } 237 | } 238 | } 239 | } 240 | } 241 | 242 | // watchService watch service change 243 | func (zd *ZookeeperDiscoverer) watchService(service *ZookeeperService) { 244 | for { 245 | _, _, event, err := zd.zkConn.GetW(service.WatchPath) 246 | if err != nil { 247 | log.Errorf("watch service: %s fail, err: %s", service.WatchPath, err) 248 | zd.removeWatchService(service) 249 | return 250 | } 251 | 252 | select { 253 | case <-service.WatchContext.Done(): 254 | log.Infof("watch service: %s cancel, err: %s", service.WatchPath, service.WatchContext.Err()) 255 | return 256 | case e := <-event: 257 | switch e.Type { 258 | case zk.EventNodeDataChanged: 259 | err = zd.fetchService(service.Name, service.BindEntities) 260 | if err != nil { 261 | log.Errorf("fetch service: %s fail, err: %s", service.WatchPath, err) 262 | } 263 | case zk.EventNodeDeleted: 264 | err = zd.removeService(service.Name, true) 265 | if err != nil { 266 | log.Errorf("remove service: %s remove fail", err) 267 | } 268 | } 269 | } 270 | } 271 | } 272 | 273 | // addWatchService remove watch service 274 | func (zd *ZookeeperDiscoverer) removeWatchService(service *ZookeeperService) { 275 | service.WatchCancel() 276 | zd.zkWatchServices.Delete(service.Name) 277 | zd.zkUnWatchServices.LoadOrStore(service.Name, service.BindEntities) 278 | log.Infof("stop watch service: %s", service.Name) 279 | } 280 | 281 | // addWatchService add watch service 282 | func (zd *ZookeeperDiscoverer) addWatchService(service *ZookeeperService) { 283 | zd.zkWatchServices.LoadOrStore(service.Name, service) 284 | zd.zkUnWatchServices.Delete(service.Name) 285 | go zd.watchService(service) 286 | log.Infof("start watch service: %s", service.Name) 287 | } 288 | -------------------------------------------------------------------------------- /internal/discoverer/zookeeper_test.go: -------------------------------------------------------------------------------- 1 | package discoverer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/api7/apisix-seed/internal/core/message" 7 | 8 | "github.com/api7/apisix-seed/internal/conf" 9 | "github.com/go-zookeeper/zk" 10 | "github.com/stretchr/testify/assert" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var zkYamlConfig = ` 15 | hosts: 16 | - "127.0.0.1:2181" 17 | prefix: /zookeeper 18 | weight: 100 19 | timeout: 10 20 | ` 21 | 22 | func getZkConfig() (*conf.Zookeeper, error) { 23 | zkConf := &conf.Zookeeper{} 24 | err := yaml.Unmarshal([]byte(zkYamlConfig), zkConf) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return zkConf, nil 29 | } 30 | 31 | func updateZkService(conn *zk.Conn, svcPath string, svcNode string) error { 32 | _, stat, err := conn.Get(svcPath) 33 | if err != nil { 34 | return err 35 | } 36 | _, err = conn.Set(svcPath, []byte(svcNode), stat.Version) 37 | return err 38 | } 39 | 40 | func createZkService(conn *zk.Conn, svcPath string, svcNode string) error { 41 | _, err := conn.Create(svcPath, []byte(svcNode), 0, zk.WorldACL(zk.PermAll)) 42 | return err 43 | } 44 | 45 | func removeZkService(conn *zk.Conn, svcPath string) error { 46 | _, stat, err := conn.Get(svcPath) 47 | if err != nil { 48 | // Does not exist and returns nil 49 | return nil 50 | } 51 | err = conn.Delete(svcPath, stat.Version) 52 | return err 53 | } 54 | 55 | func zkMsg2Value(msg *message.Message) string { 56 | str, _ := msg.Marshal() 57 | return string(str) 58 | } 59 | 60 | func newZkDiscoverer() (*ZookeeperDiscoverer, error) { 61 | config, err := getZkConfig() 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | dis, err := NewZookeeperDiscoverer(config) 67 | 68 | return dis.(*ZookeeperDiscoverer), err 69 | } 70 | 71 | func TestNewZookeeperDiscoverer(t *testing.T) { 72 | dis, err := newZkDiscoverer() 73 | assert.Nil(t, err) 74 | assert.NotNil(t, dis) 75 | } 76 | 77 | func TestZookeeperDiscoverer(t *testing.T) { 78 | dis, err := newZkDiscoverer() 79 | assert.Nil(t, err) 80 | assert.NotNil(t, dis) 81 | 82 | conn := dis.zkConn 83 | svcName := "svc" 84 | svcPath := "/zookeeper/" + svcName 85 | // clear zookeeper service 86 | err = removeZkService(conn, svcPath) 87 | assert.Nil(t, err) 88 | 89 | key := "/apisix/routes/1" 90 | value := `{"uri":"/hh","upstream":{"discovery_type":"zookeeper","service_name":"svc"}}` 91 | msg, err := message.NewMessage(key, []byte(value), 1, message.EventAdd, message.A6RoutesConf) 92 | assert.Nil(t, err) 93 | 94 | err = dis.Query(msg) 95 | assert.NotNil(t, err) 96 | msgChan := dis.Watch() 97 | 98 | // create service 99 | err = createZkService(conn, svcPath, `[{"host":"127.0.0.1","port":1980,"weight":100}]`) 100 | assert.Nil(t, err) 101 | newMsg := <-msgChan 102 | expectValue := `{"uri":"/hh","upstream":{"_discovery_type":"zookeeper","_service_name":"svc","nodes":[{"host":"127.0.0.1","port":1980,"weight":100}]}}` 103 | assert.JSONEq(t, expectValue, zkMsg2Value(newMsg)) 104 | 105 | // update service 106 | err = updateZkService(conn, svcPath, `[{"host":"127.0.0.1","port":1981,"weight":100}]`) 107 | assert.Nil(t, err) 108 | newMsg = <-msgChan 109 | expectValue = `{"uri":"/hh","upstream":{"_discovery_type":"zookeeper","_service_name":"svc","nodes":[{"host":"127.0.0.1","port":1981,"weight":100}]}}` 110 | assert.JSONEq(t, expectValue, zkMsg2Value(newMsg)) 111 | 112 | // remove service 113 | err = removeZkService(conn, svcPath) 114 | assert.Nil(t, err) 115 | newMsg = <-msgChan 116 | expectValue = `{"uri":"/hh","upstream":{"_discovery_type":"zookeeper","_service_name":"svc","nodes":[]}}` 117 | assert.JSONEq(t, expectValue, zkMsg2Value(newMsg)) 118 | } 119 | -------------------------------------------------------------------------------- /internal/utils/validate.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/xeipuuv/gojsonschema" 8 | "go.uber.org/zap/buffer" 9 | ) 10 | 11 | type Validator interface { 12 | Validate(obj interface{}) error 13 | } 14 | 15 | type JsonSchemaValidator struct { 16 | schema *gojsonschema.Schema 17 | } 18 | 19 | func NewJsonSchemaValidator(bs string) (Validator, error) { 20 | s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(bs)) 21 | if err != nil { 22 | return nil, fmt.Errorf("new schema failed: %s", err) 23 | } 24 | return &JsonSchemaValidator{ 25 | schema: s, 26 | }, nil 27 | } 28 | 29 | func (v *JsonSchemaValidator) Validate(obj interface{}) error { 30 | ret, err := v.schema.Validate(gojsonschema.NewGoLoader(obj)) 31 | if err != nil { 32 | return fmt.Errorf("validate failed: %s", err) 33 | } 34 | 35 | if !ret.Valid() { 36 | errString := buffer.Buffer{} 37 | for i, vErr := range ret.Errors() { 38 | if i != 0 { 39 | errString.AppendString("\n") 40 | } 41 | errString.AppendString(vErr.String()) 42 | } 43 | return errors.New(errString.String()) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/utils/validate_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type TestObj struct { 15 | Name string `json:"name"` 16 | Email string `json:"email"` 17 | Age int `json:"age"` 18 | } 19 | 20 | func ReadFile(t *testing.T, file string) []byte { 21 | wd, _ := os.Getwd() 22 | dir := wd[:strings.Index(wd, "internal")] 23 | path := filepath.Join(dir, "test/testdata/", file) 24 | bs, err := ioutil.ReadFile(path) 25 | assert.Nil(t, err) 26 | return bs 27 | } 28 | 29 | func TestJsonSchemaValidator_Validate(t *testing.T) { 30 | tests := []struct { 31 | givePath string 32 | giveObj interface{} 33 | wantNewErr error 34 | wantValidateErr []error 35 | }{ 36 | { 37 | givePath: "validate_test.json", 38 | giveObj: TestObj{ 39 | Name: "lessName", 40 | Email: "too long name greater than 10", 41 | Age: 12, 42 | }, 43 | wantValidateErr: []error{ 44 | fmt.Errorf("name: String length must be greater than or equal to 10\nemail: String length must be less than or equal to 10"), 45 | fmt.Errorf("email: String length must be less than or equal to 10\nname: String length must be greater than or equal to 10"), 46 | }, 47 | }, 48 | } 49 | 50 | for _, tc := range tests { 51 | bs := ReadFile(t, tc.givePath) 52 | v, err := NewJsonSchemaValidator(string(bs)) 53 | if err != nil { 54 | assert.Equal(t, tc.wantNewErr, err) 55 | continue 56 | } 57 | err = v.Validate(tc.giveObj) 58 | ret := false 59 | for _, wantErr := range tc.wantValidateErr { 60 | if wantErr.Error() == err.Error() { 61 | ret = true 62 | } 63 | } 64 | assert.True(t, ret) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "sync" 7 | "syscall" 8 | 9 | "github.com/api7/gopkg/pkg/log" 10 | rotatelogs "github.com/lestrrat-go/file-rotatelogs" 11 | "go.uber.org/zap/zapcore" 12 | 13 | "github.com/api7/apisix-seed/internal/conf" 14 | "github.com/api7/apisix-seed/internal/core/components" 15 | "github.com/api7/apisix-seed/internal/core/storer" 16 | "github.com/api7/apisix-seed/internal/discoverer" 17 | ) 18 | 19 | func initLogger(logConf *conf.Log) error { 20 | 21 | opts := []log.Option{ 22 | log.WithLogLevel(logConf.Level), 23 | log.WithSkipFrames(3), 24 | } 25 | if logConf.Path != "" { 26 | writer, err := rotatelogs.New( 27 | logConf.Path+"-%Y%m%d%H%M%S", 28 | rotatelogs.WithLinkName(logConf.Path), 29 | rotatelogs.WithMaxAge(logConf.MaxAge), 30 | rotatelogs.WithRotationSize(logConf.MaxSize), 31 | rotatelogs.WithRotationTime(logConf.RotationTime), 32 | ) 33 | if err != nil { 34 | return err 35 | } 36 | opts = append(opts, log.WithWriteSyncer(zapcore.AddSync(writer))) 37 | } else { 38 | opts = append(opts, log.WithOutputFile("stderr")) 39 | } 40 | l, err := log.NewLogger(opts...) 41 | if err != nil { 42 | return err 43 | } 44 | log.DefaultLogger = l 45 | 46 | return nil 47 | } 48 | 49 | func main() { 50 | conf.InitConf() 51 | 52 | if err := initLogger(conf.LogConfig); err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | etcdClient, err := storer.NewEtcd(conf.ETCDConfig) 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | wg := sync.WaitGroup{} 62 | wg.Add(2) 63 | go func() { 64 | defer wg.Done() 65 | err := storer.InitStores(etcdClient) 66 | if err != nil { 67 | panic(err) 68 | } 69 | }() 70 | go func() { 71 | defer wg.Done() 72 | err := discoverer.InitDiscoverers() 73 | if err != nil { 74 | panic(err) 75 | } 76 | }() 77 | wg.Wait() 78 | 79 | rewriter := components.Rewriter{ 80 | Prefix: conf.ETCDConfig.Prefix, 81 | } 82 | rewriter.Init() 83 | defer rewriter.Close() 84 | 85 | watcher := components.Watcher{} 86 | watcher.Watch() 87 | err = watcher.Init() 88 | if err != nil { 89 | log.Error(err.Error()) 90 | return 91 | } 92 | 93 | quit := make(chan os.Signal, 1) 94 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 95 | 96 | sig := <-quit 97 | log.Infof("APISIX-Seed receive %s and start shutting down", sig.String()) 98 | } 99 | -------------------------------------------------------------------------------- /test/e2e/go.mod: -------------------------------------------------------------------------------- 1 | module e2e 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-zookeeper/zk v1.0.2 7 | github.com/onsi/ginkgo/v2 v2.0.0 8 | github.com/onsi/gomega v1.18.1 9 | ) 10 | 11 | require ( 12 | github.com/kr/pretty v0.1.0 // indirect 13 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 14 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect 15 | golang.org/x/text v0.3.7 // indirect 16 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 17 | gopkg.in/yaml.v2 v2.4.0 // indirect 18 | ) 19 | 20 | replace github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 => github.com/buger/jsonparser v1.1.1 21 | -------------------------------------------------------------------------------- /test/e2e/go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 7 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 8 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 9 | github.com/go-zookeeper/zk v1.0.2 h1:4mx0EYENAdX/B/rbunjlt5+4RTA/a9SMHBRuSKdGxPM= 10 | github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 13 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 14 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 15 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 16 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 17 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 18 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 19 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 20 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 21 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 26 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 27 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 28 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 33 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 34 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 35 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 36 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 37 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 38 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 39 | github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= 40 | github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 41 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 42 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 43 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 44 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 45 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 48 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 49 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 50 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 51 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 52 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 53 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 54 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 55 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 56 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 57 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 58 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 59 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 60 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= 61 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 62 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= 80 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 82 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 83 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 84 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 85 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 86 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 87 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 88 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 89 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 90 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 91 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 94 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 95 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 96 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 97 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 98 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 99 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 100 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 101 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 102 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 103 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 104 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 105 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 106 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 108 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 109 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 110 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 111 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 112 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 113 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 114 | -------------------------------------------------------------------------------- /test/e2e/regcenter/regcenter_suite_test.go: -------------------------------------------------------------------------------- 1 | package regcenter_test 2 | 3 | import ( 4 | "e2e/tools" 5 | "testing" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = BeforeSuite(func() { 12 | Expect(tools.CleanResources("routes")).To(BeNil()) 13 | Expect(tools.CleanResources("upstreams")).To(BeNil()) 14 | Expect(tools.NewIRegCenter("nacos").Clean()).To(BeNil()) 15 | Expect(tools.NewIRegCenter("zookeeper").Clean()).To(BeNil()) 16 | }) 17 | 18 | func TestRegcenter(t *testing.T) { 19 | RegisterFailHandler(Fail) 20 | RunSpecs(t, "Regcenter Suite") 21 | } 22 | -------------------------------------------------------------------------------- /test/e2e/regcenter/regcenter_test.go: -------------------------------------------------------------------------------- 1 | package regcenter_test 2 | 3 | import ( 4 | "e2e/tools" 5 | "e2e/tools/common" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("Normal test", Ordered, func() { 15 | Context("single route, one server", func() { 16 | 17 | type normalCase struct { 18 | URI string 19 | Route *tools.Route 20 | Upstream *tools.Upstream 21 | Server *tools.SimServer 22 | Reg tools.IRegCenter 23 | ExpectBody string 24 | } 25 | 26 | DescribeTable("general logic: route with upstream", Ordered, 27 | func(tc normalCase) { 28 | Expect(tools.CreateRoutes([]*tools.Route{tc.Route})).To(BeNil()) 29 | //create sim server 30 | Expect(tools.CreateSimServer([]*tools.SimServer{tc.Server})).To(BeNil()) 31 | 32 | // upstream server online 33 | Expect(tc.Server.Register(tc.Reg)).To(BeNil()) 34 | time.Sleep(3 * time.Second) 35 | status, body, err := common.RequestDP(tc.URI) 36 | Expect(err).To(BeNil()) 37 | Expect(status).To(Equal(200)) 38 | Expect(body).To(Equal(tc.ExpectBody)) 39 | // upstream server offline 40 | Expect(tc.Server.LogOut(tc.Reg)).To(BeNil()) 41 | time.Sleep(3 * time.Second) 42 | status, _, err = common.RequestDP(tc.URI) 43 | Expect(err).To(BeNil()) 44 | Expect(status).To(Equal(503)) 45 | 46 | tools.DestroySimServer([]*tools.SimServer{tc.Server}) 47 | }, 48 | Entry("Nacos", normalCase{ 49 | URI: "/test1", 50 | Route: tools.NewRoute("1", "/test1", "APISIX-NACOS", "nacos"), 51 | Server: tools.NewSimServer("0.0.0.0", "9990", "APISIX-NACOS"), 52 | Reg: tools.NewIRegCenter("nacos"), 53 | ExpectBody: "response: 0.0.0.0:9990", 54 | }), 55 | Entry("Zookeeper", normalCase{ 56 | URI: "/test2", 57 | Route: tools.NewRoute("2", "/test2", "APISIX-ZK", "zookeeper"), 58 | Server: tools.NewSimServer("0.0.0.0", "9991", "APISIX-ZK"), 59 | Reg: tools.NewIRegCenter("zookeeper"), 60 | ExpectBody: "response: 0.0.0.0:9991", 61 | }), 62 | ) 63 | 64 | DescribeTable("general logic: route with upstream_id", Ordered, 65 | func(tc normalCase) { 66 | Expect(tools.CreateUpstreams([]*tools.Upstream{tc.Upstream})).To(BeNil()) 67 | Expect(tools.CreateRoutes([]*tools.Route{tc.Route})).To(BeNil()) 68 | //create sim server 69 | Expect(tools.CreateSimServer([]*tools.SimServer{tc.Server})).To(BeNil()) 70 | 71 | // upstream server online 72 | Expect(tc.Server.Register(tc.Reg)).To(BeNil()) 73 | time.Sleep(3 * time.Second) 74 | status, body, err := common.RequestDP(tc.URI) 75 | Expect(err).To(BeNil()) 76 | Expect(status).To(Equal(200)) 77 | Expect(body).To(Equal(tc.ExpectBody)) 78 | // upstream server offline 79 | Expect(tc.Server.LogOut(tc.Reg)).To(BeNil()) 80 | time.Sleep(3 * time.Second) 81 | status, _, err = common.RequestDP(tc.URI) 82 | Expect(err).To(BeNil()) 83 | Expect(status).To(Equal(503)) 84 | 85 | tools.DestroySimServer([]*tools.SimServer{tc.Server}) 86 | }, 87 | Entry("Nacos", normalCase{ 88 | URI: "/test3", 89 | Upstream: tools.NewUpstream("1", "APISIX-NACOS", "nacos"), 90 | Route: tools.NewRouteWithUpstreamID("1", "/test3", "1"), 91 | Server: tools.NewSimServer("0.0.0.0", "9990", "APISIX-NACOS"), 92 | Reg: tools.NewIRegCenter("nacos"), 93 | ExpectBody: "response: 0.0.0.0:9990", 94 | }), 95 | Entry("Zookeeper", normalCase{ 96 | URI: "/test4", 97 | Upstream: tools.NewUpstream("2", "APISIX-ZK", "zookeeper"), 98 | Route: tools.NewRouteWithUpstreamID("2", "/test4", "2"), 99 | Server: tools.NewSimServer("0.0.0.0", "9991", "APISIX-ZK"), 100 | Reg: tools.NewIRegCenter("zookeeper"), 101 | ExpectBody: "response: 0.0.0.0:9991", 102 | }), 103 | ) 104 | }) 105 | 106 | Context("switch discover mode and nodes mode", func() { 107 | type normalCase struct { 108 | URI string 109 | Route *tools.Route 110 | DisUpstream *tools.Upstream 111 | NodesUpstream *tools.Upstream 112 | DisServer *tools.SimServer 113 | NodesServer *tools.SimServer 114 | Reg tools.IRegCenter 115 | } 116 | 117 | discoverModeFirst := func(tc normalCase) { 118 | Expect(tools.CreateUpstreams([]*tools.Upstream{tc.DisUpstream})).To(BeNil()) 119 | Expect(tools.CreateRoutes([]*tools.Route{tc.Route})).To(BeNil()) 120 | //create sim server 121 | Expect(tools.CreateSimServer([]*tools.SimServer{tc.DisServer})).To(BeNil()) 122 | Expect(tools.CreateSimServer([]*tools.SimServer{tc.NodesServer})).To(BeNil()) 123 | // upstream server online 124 | Expect(tc.DisServer.Register(tc.Reg)).To(BeNil()) 125 | 126 | time.Sleep(3 * time.Second) 127 | status, body, err := common.RequestDP(tc.URI) 128 | Expect(err).To(BeNil()) 129 | Expect(status).To(Equal(200)) 130 | Expect(body).To(Equal("response: 0.0.0.0:" + tc.DisServer.Node.Port)) 131 | } 132 | 133 | nodesModeFirst := func(tc normalCase) { 134 | Expect(tools.CreateUpstreams([]*tools.Upstream{tc.NodesUpstream})).To(BeNil()) 135 | Expect(tools.CreateRoutes([]*tools.Route{tc.Route})).To(BeNil()) 136 | //create sim server 137 | Expect(tools.CreateSimServer([]*tools.SimServer{tc.DisServer})).To(BeNil()) 138 | Expect(tools.CreateSimServer([]*tools.SimServer{tc.NodesServer})).To(BeNil()) 139 | // upstream server online 140 | Expect(tc.DisServer.Register(tc.Reg)).To(BeNil()) 141 | 142 | time.Sleep(3 * time.Second) 143 | status, body, err := common.RequestDP(tc.URI) 144 | Expect(err).To(BeNil()) 145 | Expect(status).To(Equal(200)) 146 | expectBody := "" 147 | for k := range tc.NodesUpstream.Nodes { 148 | // host is DOCKERGATEWAY, we should replace it to 0.0.0.0 149 | port := strings.Split(k, ":")[1] 150 | expectBody = "response: 0.0.0.0:" + port 151 | } 152 | Expect(body).To(Equal(expectBody)) 153 | } 154 | 155 | changeNodes2Discover := func(tc normalCase, method string) { 156 | fmt.Println("change nodes to discover mode") 157 | if method == "PATCH" { 158 | // use _service_name instead of service_name 159 | Expect(tools.PatchUpstreams([]*tools.Upstream{tc.DisUpstream})).To(BeNil()) 160 | } else { 161 | Expect(tools.CreateUpstreams([]*tools.Upstream{tc.DisUpstream})).To(BeNil()) 162 | } 163 | 164 | time.Sleep(3 * time.Second) 165 | status, body, err := common.RequestDP(tc.URI) 166 | Expect(err).To(BeNil()) 167 | Expect(status).To(Equal(200)) 168 | Expect(body).To(Equal("response: 0.0.0.0:" + tc.DisServer.Node.Port)) 169 | } 170 | 171 | changeDiscover2Nodes := func(tc normalCase) { 172 | fmt.Println("change discover to nodes mode") 173 | // Just use PUT method, for Patch method need delete "service_name" and "discover_type" attr 174 | // it's not related to apisix-seed 175 | Expect(tools.CreateUpstreams([]*tools.Upstream{tc.NodesUpstream})).To(BeNil()) 176 | time.Sleep(3 * time.Second) 177 | status, body, err := common.RequestDP(tc.URI) 178 | Expect(err).To(BeNil()) 179 | Expect(status).To(Equal(200)) 180 | expectBody := "" 181 | for k := range tc.NodesUpstream.Nodes { 182 | // host is DOCKERGATEWAY, we should replace it to 0.0.0.0 183 | port := strings.Split(k, ":")[1] 184 | expectBody = "response: 0.0.0.0:" + port 185 | } 186 | Expect(body).To(Equal(expectBody)) 187 | } 188 | 189 | DescribeTable("discover mode to nodes to discover: discover first", Ordered, 190 | func(tc normalCase) { 191 | discoverModeFirst(tc) 192 | changeDiscover2Nodes(tc) 193 | changeNodes2Discover(tc, "PUT") 194 | changeDiscover2Nodes(tc) 195 | changeNodes2Discover(tc, "PATCH") 196 | 197 | tools.DestroySimServer([]*tools.SimServer{tc.DisServer}) 198 | tools.DestroySimServer([]*tools.SimServer{tc.NodesServer}) 199 | }, 200 | Entry("nacos", normalCase{ 201 | URI: "/test5", 202 | DisUpstream: tools.NewUpstream("1", "APISIX-NACOS", "nacos"), 203 | DisServer: tools.NewSimServer("0.0.0.0", "9990", "APISIX-NACOS"), 204 | NodesUpstream: tools.NewUpstreamWithNodes("1", "0.0.0.0", "9991"), 205 | NodesServer: tools.NewSimServer("0.0.0.0", "9991", ""), 206 | Route: tools.NewRouteWithUpstreamID("1", "/test5", "1"), 207 | Reg: tools.NewIRegCenter("nacos"), 208 | }), 209 | Entry("zookeeper", normalCase{ 210 | URI: "/test6", 211 | DisUpstream: tools.NewUpstream("1", "APISIX-ZK", "zookeeper"), 212 | DisServer: tools.NewSimServer("0.0.0.0", "9990", "APISIX-ZK"), 213 | NodesUpstream: tools.NewUpstreamWithNodes("1", "0.0.0.0", "9991"), 214 | NodesServer: tools.NewSimServer("0.0.0.0", "9991", ""), 215 | Route: tools.NewRouteWithUpstreamID("1", "/test6", "1"), 216 | Reg: tools.NewIRegCenter("zookeeper"), 217 | }), 218 | ) 219 | 220 | DescribeTable("discover mode to nodes to discover: nodes first", Ordered, 221 | func(tc normalCase) { 222 | nodesModeFirst(tc) 223 | changeNodes2Discover(tc, "PUT") 224 | changeDiscover2Nodes(tc) 225 | changeNodes2Discover(tc, "PATCH") 226 | changeDiscover2Nodes(tc) 227 | 228 | tools.DestroySimServer([]*tools.SimServer{tc.DisServer}) 229 | tools.DestroySimServer([]*tools.SimServer{tc.NodesServer}) 230 | }, 231 | Entry("nacos", normalCase{ 232 | URI: "/test7", 233 | DisUpstream: tools.NewUpstream("1", "APISIX-NACOS", "nacos"), 234 | DisServer: tools.NewSimServer("0.0.0.0", "9990", "APISIX-NACOS"), 235 | NodesUpstream: tools.NewUpstreamWithNodes("1", "0.0.0.0", "9991"), 236 | NodesServer: tools.NewSimServer("0.0.0.0", "9991", ""), 237 | Route: tools.NewRouteWithUpstreamID("1", "/test7", "1"), 238 | Reg: tools.NewIRegCenter("nacos"), 239 | }), 240 | Entry("zookeeper", normalCase{ 241 | URI: "/test8", 242 | DisUpstream: tools.NewUpstream("1", "APISIX-ZK", "zookeeper"), 243 | DisServer: tools.NewSimServer("0.0.0.0", "9990", "APISIX-ZK"), 244 | NodesUpstream: tools.NewUpstreamWithNodes("1", "0.0.0.0", "9991"), 245 | NodesServer: tools.NewSimServer("0.0.0.0", "9991", ""), 246 | Route: tools.NewRouteWithUpstreamID("1", "/test8", "1"), 247 | Reg: tools.NewIRegCenter("zookeeper"), 248 | }), 249 | ) 250 | }) 251 | }) 252 | -------------------------------------------------------------------------------- /test/e2e/tools/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | NACOS_HOST = "http://127.0.0.1:8848" 14 | ZK_HOST = "127.0.0.1:2181" 15 | APISIX_CP_HOST = "http://127.0.0.1:9180" 16 | APISIX_DP_HOST = "http://127.0.0.1:9080" 17 | APISIX_TOKEN = "edd1c9f034335f136f87ad84b625c8f1" 18 | DOCKER_GATEWAY = "172.50.238.1" 19 | ) 20 | 21 | type Node struct { 22 | Host string 23 | Port string 24 | Weight int 25 | ServiceName string 26 | Args map[string]interface{} 27 | Metadata map[string]interface{} 28 | } 29 | 30 | func (n *Node) IPPort() string { 31 | return n.Host + ":" + n.Port 32 | } 33 | 34 | func (n *Node) String() string { 35 | return "serviceName=" + n.ServiceName + 36 | " ip=" + n.Host + " port=" + n.Port 37 | } 38 | 39 | func RequestDP(uri string) (int, string, error) { 40 | url := APISIX_DP_HOST + uri 41 | req, err := http.NewRequest("GET", url, nil) 42 | if err != nil { 43 | return 0, "", err 44 | } 45 | 46 | client := &http.Client{} 47 | resp, err := client.Do(req) 48 | if err != nil { 49 | return 0, "", err 50 | } 51 | 52 | defer resp.Body.Close() 53 | body, err := ioutil.ReadAll(resp.Body) 54 | if err != nil { 55 | return resp.StatusCode, "", err 56 | } 57 | return resp.StatusCode, string(body), nil 58 | } 59 | 60 | func RequestCP(uri, method, data string) (*http.Response, error) { 61 | url := APISIX_CP_HOST + uri 62 | var body io.Reader 63 | if data != "" { 64 | body = strings.NewReader(data) 65 | } 66 | req, err := http.NewRequest(method, url, body) 67 | if err != nil { 68 | return nil, err 69 | } 70 | req.Header.Add("X-API-KEY", APISIX_TOKEN) 71 | 72 | client := &http.Client{} 73 | resp, err := client.Do(req) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | if resp.StatusCode != 201 && resp.StatusCode != 200 { 79 | defer resp.Body.Close() 80 | respBody, _ := ioutil.ReadAll(resp.Body) 81 | return nil, errors.New(fmt.Sprintf("%s %s failed: %s", method, uri, string(respBody))) 82 | } 83 | fmt.Println(method + " " + uri + " successful") 84 | return resp, nil 85 | } 86 | -------------------------------------------------------------------------------- /test/e2e/tools/regcenter.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "e2e/tools/common" 5 | "e2e/tools/regcenter" 6 | ) 7 | 8 | type IRegCenter interface { 9 | Online(*common.Node) error 10 | Offline(*common.Node) error 11 | Clean() error 12 | //Query() 13 | } 14 | 15 | func NewIRegCenter(name string) IRegCenter { 16 | switch name { 17 | case "nacos": 18 | return regcenter.NewNacos() 19 | case "zookeeper": 20 | return regcenter.NewZookeeper() 21 | } 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /test/e2e/tools/regcenter/nacos.go: -------------------------------------------------------------------------------- 1 | package regcenter 2 | 3 | import ( 4 | "e2e/tools/common" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | ) 11 | 12 | type Nacos struct { 13 | } 14 | 15 | func NewNacos() *Nacos { 16 | return &Nacos{} 17 | } 18 | 19 | type servicesResp struct { 20 | Count int `json:"count"` 21 | Doms []string `json:"doms"` 22 | } 23 | 24 | func (n *Nacos) Online(node *common.Node) error { 25 | //curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=APISIX-NACOS&ip=127.0.0.1&port=10000' 26 | url := common.NACOS_HOST + "/nacos/v1/ns/instance?healthy=true&" + 27 | "serviceName=" + node.ServiceName + "&" + 28 | "ip=" + common.DOCKER_GATEWAY + "&" + 29 | "port=" + node.Port 30 | 31 | req, err := http.NewRequest("POST", url, nil) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | client := &http.Client{} 37 | resp, err := client.Do(req) 38 | if err != nil { 39 | return err 40 | } 41 | if resp.StatusCode != 200 { 42 | return errors.New("register instance failed: " + node.String()) 43 | } 44 | 45 | fmt.Println("register instance to Nacos: ", node.String()) 46 | return nil 47 | } 48 | 49 | func (n *Nacos) Offline(node *common.Node) error { 50 | url := common.NACOS_HOST + "/nacos/v1/ns/instance?" + 51 | "serviceName=" + node.ServiceName + "&" + 52 | "ip=" + common.DOCKER_GATEWAY + "&" + 53 | "port=" + node.Port 54 | 55 | req, err := http.NewRequest("DELETE", url, nil) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | client := &http.Client{} 61 | resp, err := client.Do(req) 62 | if err != nil { 63 | return err 64 | } 65 | if resp.StatusCode != 200 { 66 | return errors.New("delete instance failed: " + node.String()) 67 | } 68 | 69 | fmt.Println("offline instance to Nacos: ", node.String()) 70 | return nil 71 | } 72 | 73 | func (n *Nacos) getServices() ([]string, error) { 74 | // curl -X GET '127.0.0.1:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=2' 75 | // we just get 10 services, it's enough 76 | url := common.NACOS_HOST + "/nacos/v1/ns/service/list?pageNo=1&pageSize=10" 77 | req, err := http.NewRequest("GET", url, nil) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | client := &http.Client{} 83 | resp, err := client.Do(req) 84 | defer resp.Body.Close() 85 | body, err := ioutil.ReadAll(resp.Body) 86 | servResp := &servicesResp{} 87 | err = json.Unmarshal(body, servResp) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if servResp.Count > len(servResp.Doms) { 92 | 93 | } 94 | return servResp.Doms, nil 95 | } 96 | 97 | func (n *Nacos) deleteService(service string) error { 98 | // curl -X DELETE '127.0.0.1:8848/nacos/v1/ns/service?serviceName=APISIX-NACOS' 99 | url := common.NACOS_HOST + "/nacos/v1/ns/service?serviceName=" + service 100 | req, err := http.NewRequest("DELETE", url, nil) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | client := &http.Client{} 106 | resp, err := client.Do(req) 107 | if err != nil { 108 | return err 109 | } 110 | if resp.StatusCode != 200 { 111 | return errors.New("delete service failed, serviceName=:" + service) 112 | } 113 | fmt.Println("delete service, serviceName=" + service) 114 | return nil 115 | } 116 | 117 | func (n *Nacos) Clean() error { 118 | fmt.Println("clean all service form nacos...") 119 | services, err := n.getServices() 120 | if err != nil { 121 | panic(err) 122 | } 123 | for _, srv := range services { 124 | err = n.deleteService(srv) 125 | if err != nil { 126 | return err 127 | } 128 | } 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /test/e2e/tools/regcenter/zookeeper.go: -------------------------------------------------------------------------------- 1 | package regcenter 2 | 3 | import ( 4 | "e2e/tools/common" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-zookeeper/zk" 9 | ) 10 | 11 | type Zookeeper struct { 12 | conn *zk.Conn 13 | prefix string 14 | } 15 | 16 | func NewZookeeper() *Zookeeper { 17 | conn, _, err := zk.Connect([]string{common.ZK_HOST}, time.Second*5) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return &Zookeeper{ 22 | conn: conn, 23 | prefix: "/zookeeper", 24 | } 25 | } 26 | 27 | func (zookeeper *Zookeeper) Online(node *common.Node) error { 28 | nodeStr := `[{"host":"` + common.DOCKER_GATEWAY + `","port":` + node.Port + `}]` 29 | // zk does not allow duplicate registration 30 | path := zookeeper.prefix + "/" + node.ServiceName 31 | ret, _, err := zookeeper.conn.Get(path) 32 | if ret != nil { 33 | return nil 34 | } 35 | _, err = zookeeper.conn.Create(path, []byte(nodeStr), 0, zk.WorldACL(zk.PermAll)) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | fmt.Println("register instance to Zookeeper: ", node.String()) 41 | return err 42 | } 43 | 44 | func (zookeeper *Zookeeper) Offline(node *common.Node) error { 45 | path := zookeeper.prefix + "/" + node.ServiceName 46 | _, stat, err := zookeeper.conn.Exists(path) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | fmt.Println("offline instance to Zookeeper: ", node.String()) 52 | return zookeeper.conn.Delete(path, stat.Version) 53 | 54 | } 55 | 56 | func (zookeeper *Zookeeper) Clean() error { 57 | fmt.Println("clean all service form zookeeper...") 58 | children, stat, err := zookeeper.conn.Children(zookeeper.prefix) 59 | if err != nil { 60 | return err 61 | } 62 | for _, p := range children { 63 | if p == "config" || p == "quota" { 64 | continue 65 | } 66 | if err := zookeeper.conn.Delete(zookeeper.prefix+"/"+p, stat.Version); err != nil { 67 | return err 68 | } 69 | fmt.Println("delete service, serviceName=", p) 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /test/e2e/tools/routes.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "e2e/tools/common" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | ) 9 | 10 | type Upstream struct { 11 | ID string 12 | LBType string `json:"type"` 13 | ServiceName string `json:"service_name,omitempty"` 14 | DupServiceName string `json:"_service_name,omitempty"` 15 | DiscoveryType string `json:"discovery_type,omitempty"` 16 | DupDiscoveryType string `json:"_discovery_type,omitempty"` 17 | DiscoveryArgs map[string]interface{} 18 | Nodes map[string]int `json:"nodes,omitempty"` 19 | } 20 | 21 | func (up *Upstream) Marshal() string { 22 | str, _ := json.Marshal(up) 23 | return string(str) 24 | } 25 | 26 | func (up *Upstream) Do(method string) error { 27 | resp, err := common.RequestCP("/apisix/admin/upstreams/"+up.ID, method, up.Marshal()) 28 | defer resp.Body.Close() 29 | body, _ := ioutil.ReadAll(resp.Body) 30 | fmt.Println("resp data: ", string(body)) 31 | return err 32 | } 33 | 34 | func NewUpstream(id, serviceName, regType string) *Upstream { 35 | return &Upstream{ 36 | ID: id, 37 | LBType: "roundrobin", 38 | ServiceName: serviceName, 39 | DiscoveryType: regType, 40 | DiscoveryArgs: make(map[string]interface{}), 41 | } 42 | } 43 | 44 | func NewUpstreamWithNodes(id string, host, port string) *Upstream { 45 | return &Upstream{ 46 | ID: id, 47 | Nodes: map[string]int{ 48 | common.DOCKER_GATEWAY + ":" + port: 1, 49 | }, 50 | } 51 | } 52 | 53 | type Route struct { 54 | URI string `json:"uri"` 55 | ID string 56 | UpstreamID string `json:"upstream_id,omitempty"` 57 | Upstream *Upstream `json:"upstream,omitempty"` 58 | } 59 | 60 | func (r *Route) Marshal() string { 61 | str, _ := json.Marshal(r) 62 | return string(str) 63 | } 64 | 65 | func (r *Route) Do() error { 66 | _, err := common.RequestCP("/apisix/admin/routes/"+r.ID, "PUT", r.Marshal()) 67 | return err 68 | } 69 | 70 | func NewRoute(id, uri, serviceName, regType string) *Route { 71 | return &Route{ 72 | URI: uri, 73 | ID: id, 74 | Upstream: NewUpstream("", serviceName, regType), 75 | } 76 | } 77 | 78 | func NewRouteWithUpstreamID(id, uri, uid string) *Route { 79 | return &Route{ 80 | URI: uri, 81 | ID: id, 82 | UpstreamID: uid, 83 | } 84 | } 85 | 86 | type routesResp struct { 87 | List []struct { 88 | Value struct { 89 | ID string `json:"ID"` 90 | } `json:"value"` 91 | } `json:"list"` 92 | } 93 | 94 | func getResourcesID(resource string) ([]string, error) { 95 | resp, err := common.RequestCP("/apisix/admin/"+resource, "GET", "") 96 | if err != nil { 97 | return nil, err 98 | } 99 | defer resp.Body.Close() 100 | body, err := ioutil.ReadAll(resp.Body) 101 | rouResp := &routesResp{} 102 | err = json.Unmarshal(body, rouResp) 103 | if err != nil { 104 | if _, ok := err.(*json.UnmarshalTypeError); ok { 105 | return []string{}, nil 106 | } 107 | return nil, err 108 | } 109 | keys := make([]string, len(rouResp.List)) 110 | for i, v := range rouResp.List { 111 | keys[i] = v.Value.ID 112 | } 113 | return keys, nil 114 | } 115 | 116 | func deleteResource(resource, id string) error { 117 | _, err := common.RequestCP("/apisix/admin/"+resource+"/"+id, "DELETE", "") 118 | return err 119 | } 120 | 121 | func CleanResources(resource string) error { 122 | fmt.Println("clean all routes from etcd...") 123 | ids, err := getResourcesID(resource) 124 | if err != nil { 125 | return err 126 | } 127 | for _, id := range ids { 128 | err = deleteResource(resource, id) 129 | if err != nil { 130 | return err 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | func CreateRoutes(routes []*Route) error { 137 | for _, r := range routes { 138 | err := r.Do() 139 | if err != nil { 140 | return err 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | func CreateUpstreams(upstreams []*Upstream) error { 147 | for _, up := range upstreams { 148 | err := up.Do("PUT") 149 | if err != nil { 150 | return err 151 | } 152 | } 153 | return nil 154 | } 155 | 156 | func PatchUpstreams(upstreams []*Upstream) error { 157 | for _, up := range upstreams { 158 | dupUp := &Upstream{ 159 | ID: up.ID, 160 | LBType: up.LBType, 161 | DupDiscoveryType: up.DiscoveryType, 162 | DupServiceName: up.ServiceName, 163 | DiscoveryArgs: up.DiscoveryArgs, 164 | Nodes: up.Nodes, 165 | } 166 | 167 | err := dupUp.Do("PATCH") 168 | if err != nil { 169 | return err 170 | } 171 | } 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /test/e2e/tools/servers.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "e2e/tools/common" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "sync" 9 | ) 10 | 11 | type SimServer struct { 12 | *common.Node 13 | running bool 14 | srv http.Server 15 | } 16 | 17 | func (server *SimServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | w.Header().Add("X-Server", "APISIX Test Server") 19 | w.Write([]byte("response: " + server.IPPort())) 20 | } 21 | 22 | func (server *SimServer) Run() { 23 | wg := sync.WaitGroup{} 24 | wg.Add(1) 25 | go func() { 26 | server.srv = http.Server{ 27 | Addr: server.IPPort(), 28 | Handler: server, 29 | } 30 | server.running = true 31 | fmt.Println("APISIX Test Server start: ", server.IPPort()) 32 | wg.Done() 33 | server.srv.ListenAndServe() 34 | server.running = false 35 | fmt.Println("APISIX Test Server stop: ", server.IPPort()) 36 | }() 37 | wg.Wait() 38 | } 39 | func (server *SimServer) Register(reg IRegCenter) error { 40 | return reg.Online(server.Node) 41 | } 42 | 43 | func (server *SimServer) LogOut(reg IRegCenter) error { 44 | return reg.Offline(server.Node) 45 | } 46 | 47 | func (server *SimServer) Stop() { 48 | server.srv.Close() 49 | } 50 | 51 | func (server *SimServer) Running() bool { 52 | return server.running 53 | } 54 | 55 | func NewSimServer(host, port, serviceName string) *SimServer { 56 | return &SimServer{ 57 | Node: &common.Node{ 58 | Host: host, 59 | Port: port, 60 | Weight: 1, 61 | ServiceName: serviceName, 62 | Args: make(map[string]interface{}), 63 | Metadata: make(map[string]interface{}), 64 | }, 65 | running: false, 66 | } 67 | } 68 | 69 | func CreateSimServer(servers []*SimServer) error { 70 | for _, s := range servers { 71 | s.Run() 72 | } 73 | 74 | for _, s := range servers { 75 | if !s.Running() { 76 | return errors.New(fmt.Sprintf("APISIX Test Server start failed: %s", s.IPPort())) 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | func DestroySimServer(servers []*SimServer) { 83 | for _, s := range servers { 84 | s.Stop() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/testdata/nacos_conf/default_value.yaml: -------------------------------------------------------------------------------- 1 | host: 2 | - "http://127.0.0.1:8848" 3 | -------------------------------------------------------------------------------- /test/testdata/nacos_conf/discoverer.yaml: -------------------------------------------------------------------------------- 1 | host: 2 | - "http://console.nacos.io:80" 3 | prefix: ~ 4 | -------------------------------------------------------------------------------- /test/testdata/nacos_conf/empty.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api7/apisix-seed/bd8fb778b32a3c18a01685fb88729935307ef54f/test/testdata/nacos_conf/empty.yaml -------------------------------------------------------------------------------- /test/testdata/nacos_conf/empty_host.yaml: -------------------------------------------------------------------------------- 1 | host: [] 2 | -------------------------------------------------------------------------------- /test/testdata/nacos_conf/host.yaml: -------------------------------------------------------------------------------- 1 | host: 2 | - "https://username:password@console.nacos.io:8858" 3 | -------------------------------------------------------------------------------- /test/testdata/nacos_conf/minimum.yaml: -------------------------------------------------------------------------------- 1 | host: 2 | - "http://127.0.0.1:8848" 3 | weight: -1 4 | timeout: 5 | connect: -1 6 | -------------------------------------------------------------------------------- /test/testdata/nacos_conf/pattern.yaml: -------------------------------------------------------------------------------- 1 | host: 2 | - http:# 3 | prefix: /nacos/# 4 | -------------------------------------------------------------------------------- /test/testdata/nacos_conf/set_value.yaml: -------------------------------------------------------------------------------- 1 | host: 2 | - "http://127.0.0.1:8858" 3 | prefix: /nacos/v2/ 4 | weight: 10 5 | timeout: 6 | connect: 200 7 | send: 200 8 | read: 500 9 | -------------------------------------------------------------------------------- /test/testdata/validate_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "minLength": 10 8 | }, 9 | "email": { 10 | "type": "string", 11 | "maxLength": 10 12 | }, 13 | "age": { 14 | "type": "integer", 15 | "minimum": 0 16 | } 17 | }, 18 | "additionalProperties": false 19 | } 20 | --------------------------------------------------------------------------------