From 3d1ba3c07bf2578066102f0453ba83da63d1bdb5 Mon Sep 17 00:00:00 2001
From: litesun <31329157+LiteSun@users.noreply.github.com>
Date: Wed, 26 Aug 2020 18:19:48 +0800
Subject: [PATCH 1/3] merge master (#1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* add: Determine duplicate names api for route & upstream (#305)
* fix: transaction in routes and upstreams (#306)
* add transaction for ssl and consumer (#308)
* update ci/cd for api (#307)
* update github actions for api ci cd
* fix: working-directory
* fix error
* fix: step name
* fix: mysql config for github action
* test
* use default config
* test: add e2e test for ssl and consumer (#309)
* test: add e2e test for ssl and consumer
* fix: change assert to avoid the mutual influence of route and service test
* remove useless code
* Feat: added Route Consumer and Upstream (#304)
* feat: added routes
* feat: added Consumer
* feat: added upstream
* feat: update SSL
* fix: routes
* feat: added commit command
* feat(route): set empty array for upstreamHeaderList
* fix: e2e test use the same function to set up router (#310)
* fix: return all objects when search route & upstream (#311)
* fix: route search
* fix: upstream search
* fix(deploy): added missing yarn.lock
* fix: proxy-rewrite plugin in upstream (#312)
* fix(SSL): search api
* docs: added tips when deployment
* feat(Deploy): use node alpine image
* fix(Route): set required field for custom redirect
* fix(Route): check if redirect is empty object
* fix(Deploy): add Python installation in dockerfile (#316)
Signed-off-by: imjoey
* fix(Route): update desc for status code
* fix: proxy-path default type is static (#318)
* add proxyRewrite test (#319)
* feat: bump dependencies version (#320)
* feat(Deploy): update Dockerfile
* feat(Deploy): update Deploy Dockerfile
* feat(Pages): update pages (#324)
* feat(Pages): update pages
* chore: update routes
* fix(Route): omit upstream_id when not exist
* i18n consumer (#325)
* i18n ssl (#335)
* nationalization PluginPage component (#323)
* i18n upstream (#334)
* feat(i18n): set module (#336)
* i18n set
* change set to setting
* feat(i18n): metrics module (#326)
* i18n metrics
* combine import
* feat(i18n): route module (#327)
* i18n route
* combine import
* doc: sync config.yaml from the latest version of APISIX (#344)
* i18n route (#342)
* i18n actionbar (#343)
* fix: transform vars error (#347)
* feat(i18n): pluginpage component (#345)
* i18n pluginpage
* change pluginpage to PluginPage
* feature: support run in mac system (#349)
* combine import (#348)
* i18n menu (#351)
* i18n PluginPage (#350)
* feat: prepare to release (#352)
* feat(ManagerAPI): added ASF header
* feat(FE): aded ASF Header
* feat(FE): added ASF header
* fix(FE): update PluginDrawer
* feat: remove some images
* feat: added LICENSE
* feat: update Version
* feat: added NOTICE & CODE_OF_CONDUCT
* feat: added initial CHANGELOG
* feat: rename CODE_OF_CONDUCT
* feat: revert version
* feat: update LICENSE
* feat: update License
* feat(conf): update default preview API (#353)
* doc: add install doc for manager-api (#355)
* doc: add install doc for manager-api
* doc: modify folder from build to run
* doc: add ASF header
* fix(ci): resolve lint failures (#354)
* fix(deploy): failed to start manager_api (#363)
Signed-off-by: imjoey
* feat(i18n): modify some i18n according to the proposal#331 (#366)
* Create CONTRIBUTING.md (#368)
* Create CONTRIBUTING.md
* Create ISSUE_TEMPLATE
* Create PULL_REQUEST_TEMPLATE
* doc: remove all ‘incubator’ (#367)
* feat(deploy): set gen-config-yaml.sh executable (#362)
This also would simplify the docs.
Signed-off-by: imjoey
* feat(i18n): Use auto load i18n (#332) (#371)
* Create ci.yml (#372)
* feat: release 1.5 (#364)
* Feat release 1.5 (#358)
* feat(doc): update README
* feat: update CHANGELOG
* doc: add usage of dashbaord
* Revert "doc: add usage of dashbaord"
This reverts commit 5a08c7f43539a44cd0cf0f6175574e59efbd0ab6.
* feat(Doc): update deployment
* feat(Doc): update the deployment
* feat(Doc): update the deployment
* feat: remove incubator text
* doc: modify doc for manager-api runing in local
* feat(Doc): update README
* doc: check env variables and give run.sh power to execute
* feat(Doc): update Deployment
* feat(Doc): update deployment
* doc: modify manager-api build
* feat: update ignore file
Co-authored-by: kv
Co-authored-by: 琚致远
* feat: cherry-pick 4fd0ce79bb34dbe8c31b7a27884930e3b0e5437c
* feat(compose): remove images
* feat: added line
Co-authored-by: kv
Co-authored-by: 琚致远
* feat: Unified access entrance, only the dashboard port is exposed to … (#370)
* feat: Unified access entrance, only the dashboard port is exposed to the outside
* add EOL
* docs: create I18N_USER_GUIDE.md (#373)
* docs: create I18N_USER_GUIDE.md
* docs: modify I18N_USER_GUIDE.md
* feat(Doc): added deploy doc for docker (#376)
* feat(Doc): added deploy doc for docker
* feat: added CD
* feat(Netlify): added proxy
* feat: update API
* feat: remove console
* feat(Netlify): update redirect rule
* feat: update README
* feat: update README
* update go module proxy (#378)
* Update README.md (#379)
* Update README.md
* Update README.md
* Create Preview.md
* feat(Doc): added snapshots for Preview
* feat(Doc): update images
* feat(Doc): update images
* Update README.md
* Update netlify.toml
* feat(route): route add params mapping feature (#375) (#377)
* feat(doc): update deploy manually doc
* fix: mv config.yml to config-default.yml in the latest version of apisix (#383)
* fix: wget config-default.yaml the output file need to be named config.yaml (#384)
* fix #386 wget special output file use -O (#387)
* feat(authentication): create authentication module (#330)
* feat(authentication): create module typing definition
* feat(authentication): create Login page
* feat(authentication): update typing definition
* feat(authentication): add centent to Login page
* feat(authentication): update typing definition
* feat(authentication): update Login page to add Password and Test method
* feat(authentication): update typing definition to add check and submit function
* feat(authentication): move Test login method to Example
* feat(authentication): add check and submit function
* feat(authentication): add submit function in Login page
* feat(authentication): add test to Password login method
* feat(authentication): change example LoginMethod text
* feat(authentication): add i18n content
* feat(authentication): redirect to index when login success
* feat(i18n): update i18n file import
remove import i18n file of user module manually and try auto import by umi.js
* feat(authentication): create authentication configure items
* fix(authentication): fix logging filter
write back request body for read by PostForm function
* feat(authentication): create authentication controller
* feat(authentication): update dependencies
* fix(authentication): fix logging filter
* feat(authentication): change to session for authentication
* feat(authentication): create authentication filter
use authentication filter to check every request
* feat(authentication): create unit test case
* fix(authentication): change HTTP code when authentication fail request
* feat(authentication): add jwt dependency
* feat(authentication): create session configures
* feat(authentication): change cookie-based session to jwt
* feat(authentication): change cors Access-Control-Allow-Headers header
* feat(authentication): change login page path and error handler
* feat(authentication): create request interceptor to add Authorization header
* feat(authentication): connect to backend login API and i18n
* feat(authentication): create logout page
* feat(authentication): add redirect query to back previous page
* feat(authentication): update LoginMethod definition for logout
* feat(authentication): add logout button
* feat(authentication): improve login page
* fix: clean codes
* fix(authentication): fix unit test crash
* feat(authentication): remove API url setting
* feat(authentication): improve session check
* feat(authentication): redirect to login page when not exist token
* fix: clean codes and add ASF header
* feat(User): update prefix
* fix(ci): fix preview environment (#388)
* fix README typo (#389)
* fix(ci): fix read configuration file path in docker (#390)
* doc: Introducing manager-api (#391)
* Update nginx.conf
* Update Dockerfile
* Revert "Update Dockerfile"
This reverts commit ea827bfd2789c2d939a2517b279170cccdadf35b.
* fix: preview mysql pwd was wrong (#393)
* README in Chinese (#398)
* feat(doc): added Chinese version of README
* fix(README.zh-CN.md): fix wrong link
* fix(README.zh-CN.md): add link to README.md
* fix(README.zh-CN.md): sync with README.md
* fix(README.zh-CN.md): Fix some translation errors
* fix: dashboard /user/login get error code 405 (#397)
* fix: fix dashboard /user/login get error code 405
* fix: modify nginx according to giphoo proposal
* fix(authentication): change Apache APISIX copyright (#401)
* fix: configure only necessary items, such as etcd host (#405)
* fix: configure only necessary items, such as etcd host
* fix: configure only necessary items, such as etcd host
* fix end of line
* fix: using default admin key (#408)
* fix: we need conf.json when deploying manager-api in local (#409)
* fix: we need conf.json when deploying manager-api in loal
* fix: log error when starting manager failed
* fix: click create ssl prestep not response (#407)
* fix: submit setting grafanaURl without validation (#413)
* feat: support generate `script` for APISIX (#411)
* feat: support generate `script` for APISIX
* not run in `/root` dir
* add `config.yaml` for APISIX
* fix path
* fix(authentication): change login api url (#414)
* fix(authentication): change manager API login path
* fix(authentication): change authentication unit test
* fix(authentication): clean nginx.conf codes
* fix(authentication): change login URL of front end
* fix(authentication): change authentication filter rule
Co-authored-by: kv
Co-authored-by: nic-chen <33000667+nic-chen@users.noreply.github.com>
Co-authored-by: 琚致远
Co-authored-by: juzhiyuan
Co-authored-by: Joey
Co-authored-by: bzp2010
Co-authored-by: TikWind <65604564+TikWind@users.noreply.github.com>
Co-authored-by: Lien
Co-authored-by: Rapiz
Co-authored-by: liuxiran
Co-authored-by: jie
Co-authored-by: Rapiz
Co-authored-by: 琚致远
Co-authored-by: Tusdasa翼
Co-authored-by: Shuyang Wu
Co-authored-by: Baoyuan
---
.eslintrc.js | 16 +
.github/ISSUE_TEMPLATE | 23 +
.github/PULL_REQUEST_TEMPLATE | 18 +
.github/apisix-config.yaml | 27 +
.github/workflows/api_ci.yml | 76 +
.github/workflows/api_cicd.yml | 77 +
.github/workflows/api_ut.yml | 18 -
.github/workflows/ci.yml | 32 +
.github/workflows/{api_cd.yml => deploy.yml} | 14 +-
.gitignore | 5 +-
.prettierrc.js | 16 +
.stylelintrc.js | 16 +
CHANGELOG.md | 47 +
CODE_OF_CONDUCT.md | 105 +
CONTRIBUTING.md | 43 +
Dockerfile | 26 +-
I18N_USER_GUIDE.md | 147 +
LICENSE | 227 +
NOTICE | 5 +
README-dashboard.md | 47 -
README.md | 115 +-
README.zh-CN.md | 128 +
USER_GUIDE.md | 33 +
api/Dockerfile | 22 +-
api/README.md | 6 +-
api/build.sh | 29 +-
api/conf/conf.go | 37 +-
api/conf/conf.json | 28 +-
api/{conf.json => conf/conf_preview.json} | 18 +-
api/docker-compose.yml | 16 +
api/errno/error.go | 115 +-
api/filter/authentication.go | 66 +
api/filter/cors.go | 2 +-
api/filter/logging.go | 5 +
api/go.mod | 8 +-
api/go.sum | 46 +
api/main.go | 29 +-
api/route/authentication.go | 68 +
api/route/authentication_test.go | 52 +
api/route/base.go | 51 +
api/route/base_test.go | 41 +
api/route/consumer.go | 47 +-
api/route/consumer_test.go | 98 +
api/route/route.go | 221 +-
api/route/ssl.go | 69 +-
api/route/ssl_test.go | 57 +
api/route/upstream.go | 265 +-
api/route/zclear_test.go | 37 +
api/run/run.sh | 50 +
api/script/db/schema.sql | 5 +-
api/service/consumer.go | 130 +-
api/service/consumer_test.go | 4 +-
api/service/route.go | 86 +-
api/service/route_test.go | 159 +
api/service/ssl.go | 245 +-
api/service/ssl_test.go | 88 +-
api/service/upstream.go | 43 +-
compose/README.md | 77 +-
compose/apisix_conf/config.yaml | 147 +-
compose/dashboard_conf/nginx.conf | 22 +
compose/docker-compose.yml | 21 +
.../provisioning/dashboards/all.yaml | 16 +
.../provisioning/datasources/all.yaml | 16 +
compose/manager_conf/build.sh | 19 +-
compose/pics/grafana_1.png | Bin 57816 -> 0 bytes
compose/pics/grafana_2.png | Bin 127932 -> 0 bytes
compose/pics/grafana_3.png | Bin 103384 -> 0 bytes
compose/pics/grafana_4.png | Bin 82558 -> 0 bytes
compose/pics/grafana_5.png | Bin 105366 -> 0 bytes
compose/pics/grafana_6.png | Bin 179132 -> 0 bytes
compose/pics/login.png | Bin 84958 -> 0 bytes
compose/prometheus_conf/prometheus.yml | 16 +
config/config.ts | 18 +-
config/defaultSettings.ts | 16 +
config/proxy.ts | 41 +-
config/routes.ts | 135 +-
docker/nginx.conf | 6 +-
images/manager-api.png | Bin 0 -> 44615 bytes
images/metrics-cn.png | Bin 0 -> 168247 bytes
images/metrics-en.png | Bin 0 -> 173219 bytes
images/route-create-done-list-cn.png | Bin 0 -> 198788 bytes
images/route-create-done-list-en.png | Bin 0 -> 194466 bytes
images/route-create-step1-cn.png | Bin 0 -> 249406 bytes
images/route-create-step1-en.png | Bin 0 -> 255509 bytes
images/route-create-step2-cn.png | Bin 0 -> 240242 bytes
images/route-create-step2-en.png | Bin 0 -> 250320 bytes
images/route-create-step3-cn.png | Bin 0 -> 272421 bytes
images/route-create-step3-en.png | Bin 0 -> 281334 bytes
images/route-create-step4-cn.png | Bin 0 -> 249683 bytes
images/route-create-step4-en.png | Bin 0 -> 259689 bytes
images/route-list-en.png | Bin 0 -> 183974 bytes
images/setting-cn.png | Bin 0 -> 154318 bytes
images/setting-en.png | Bin 0 -> 159045 bytes
images/ssl-list-cn.png | Bin 0 -> 175245 bytes
images/ssl-list-en.png | Bin 0 -> 177151 bytes
jest.config.js | 16 +
licenses/LICENSE-ant-design-pro.txt | 21 +
licenses/LICENSE-dag-to-lua.txt | 204 +
manager-api.md | 37 +
mock/notices.ts | 114 +-
mock/route.ts | 18 +-
mock/user.ts | 168 +-
netlify.toml | 23 +-
package.json | 54 +-
public/home_bg.png | Bin 203330 -> 0 bytes
public/icons/icon-128x128.png | Bin 1329 -> 0 bytes
public/icons/icon-192x192.png | Bin 1856 -> 0 bytes
public/icons/icon-512x512.png | Bin 5082 -> 0 bytes
public/pro_icon.svg | 1 -
src/access.ts | 20 +-
src/app.tsx | 102 +-
src/components/ActionBar/ActionBar.tsx | 67 +
src/components/ActionBar/index.ts | 19 +
src/components/ActionBar/locales/en-US.ts | 20 +
src/components/ActionBar/locales/zh-CN.ts | 20 +
src/components/Footer/index.tsx | 18 +-
src/components/HeaderDropdown/index.less | 23 +
src/components/HeaderDropdown/index.tsx | 23 +
src/components/NoticeIcon/NoticeList.less | 23 +
src/components/NoticeIcon/NoticeList.tsx | 25 +-
src/components/NoticeIcon/index.less | 23 +
src/components/NoticeIcon/index.tsx | 25 +-
src/components/PageLoading/index.tsx | 23 +
src/components/PanelSection/index.tsx | 32 +
src/components/PluginForm/PluginForm.tsx | 16 +
src/components/PluginForm/README.md | 18 +
src/components/PluginForm/data.ts | 16 +
src/components/PluginForm/index.ts | 16 +
src/components/PluginForm/locales/en-US.ts | 54 +-
src/components/PluginForm/locales/zh-CN.ts | 54 +-
src/components/PluginForm/service.ts | 16 +
src/components/PluginForm/transformer.ts | 16 +
src/components/PluginForm/typing.d.ts | 16 +
src/components/PluginModal/index.tsx | 21 +-
src/components/PluginPage/PluginCard.tsx | 42 +
src/components/PluginPage/PluginDrawer.tsx | 119 +
src/components/PluginPage/PluginPage.tsx | 143 +
src/components/PluginPage/data.ts | 113 +
src/components/PluginPage/index.ts | 22 +
src/components/PluginPage/locales/en-US.ts | 188 +
src/components/PluginPage/locales/zh-CN.ts | 188 +
src/components/PluginPage/service.ts | 45 +
src/components/PluginPage/typing.d.ts | 30 +
.../RightContent/AvatarDropdown.tsx | 46 +-
src/components/RightContent/index.less | 23 +
src/components/RightContent/index.tsx | 25 +-
src/constants.ts | 33 +
src/e2e/__mocks__/antd-pro-merge-less.js | 23 +
src/e2e/baseLayout.e2e.js | 23 +
src/global.less | 16 +
src/global.tsx | 16 +
src/helpers.tsx | 99 +
src/iconfont.ts | 24 +
src/locales/en-US.ts | 22 +-
src/locales/en-US/component.ts | 27 +-
src/locales/en-US/globalHeader.ts | 16 +
src/locales/en-US/menu.ts | 27 +-
src/locales/en-US/pwa.ts | 16 +
src/locales/en-US/setting.ts | 21 +-
src/locales/en-US/settingDrawer.ts | 16 +
src/locales/zh-CN.ts | 22 +-
src/locales/zh-CN/component.ts | 27 +-
src/locales/zh-CN/globalHeader.ts | 16 +
src/locales/zh-CN/menu.ts | 28 +-
src/locales/zh-CN/pwa.ts | 16 +
src/locales/zh-CN/setting.ts | 29 +-
src/locales/zh-CN/settingDrawer.ts | 16 +
src/pages/404.tsx | 23 +
src/pages/Consumer/Create.tsx | 116 +
src/pages/Consumer/List.tsx | 108 +
src/pages/Consumer/components/Preview.tsx | 38 +
src/pages/Consumer/components/Step1.tsx | 64 +
src/pages/Consumer/index.ts | 16 +
src/pages/Consumer/locales/en-US.ts | 46 +
src/pages/Consumer/locales/zh-CN.ts | 46 +
src/pages/Consumer/service.ts | 46 +
src/pages/Consumer/typing.d.ts | 31 +
src/pages/Metrics/Metrics.tsx | 45 +-
src/pages/Metrics/index.ts | 18 +
src/pages/Metrics/locales/en-US.ts | 21 +
src/pages/Metrics/locales/zh-CN.ts | 21 +
src/pages/Metrics/service.ts | 20 +
src/pages/{Routes => Route}/Create.less | 16 +
src/pages/{Routes => Route}/Create.tsx | 155 +-
src/pages/Route/List.tsx | 132 +
.../components/CreateStep4/CreateStep4.tsx | 56 +
.../Route/components/CreateStep4/index.ts | 17 +
.../components/ResultView/ResultView.tsx | 46 +
.../Route/components/ResultView/index.ts | 17 +
.../components/Step1/MatchingRulesView.tsx | 261 +
src/pages/Route/components/Step1/MetaView.tsx | 51 +
.../components/Step1/RequestConfigView.tsx | 122 +-
.../components/Step1/index.tsx | 18 +-
.../Step2/HttpHeaderRewriteView.tsx | 81 +-
.../components/Step2/RequestRewriteView.tsx | 315 +
src/pages/Route/components/Step2/index.tsx | 36 +
src/pages/{Routes => Route}/constants.ts | 24 +-
src/pages/Route/index.ts | 16 +
src/pages/Route/locales/en-US.ts | 163 +
src/pages/Route/locales/zh-CN.ts | 158 +
src/pages/Route/service.ts | 81 +
src/pages/{Routes => Route}/transform.ts | 70 +-
src/pages/{Routes => Route}/typing.d.ts | 31 +-
src/pages/Routes/List.tsx | 82 -
.../Routes/components/ActionBar/ActionBar.tsx | 46 -
.../Routes/components/ActionBar/index.ts | 1 -
.../components/CreateStep3/CreateStep3.tsx | 96 -
.../components/CreateStep3/PluginCard.tsx | 26 -
.../components/CreateStep3/PluginDrawer.tsx | 73 -
.../Routes/components/CreateStep3/index.ts | 1 -
.../components/CreateStep4/CreateStep4.tsx | 35 -
.../Routes/components/CreateStep4/index.ts | 1 -
.../Routes/components/PanelSection/index.tsx | 16 -
.../components/ResultView/ResultView.tsx | 24 -
.../Routes/components/ResultView/index.ts | 1 -
.../components/Step1/MatchingRulesView.tsx | 220 -
.../Routes/components/Step1/MetaView.tsx | 33 -
.../components/Step2/RequestRewriteView.tsx | 183 -
src/pages/Routes/components/Step2/index.tsx | 20 -
src/pages/Routes/service.ts | 24 -
src/pages/{ssl => SSL}/Create.less | 16 +
src/pages/SSL/Create.tsx | 106 +
src/pages/SSL/List.tsx | 151 +
.../SSL/components/CertificateForm/index.tsx | 87 +
.../components/CertificateUploader/index.tsx | 43 +-
src/pages/SSL/components/Step1/index.tsx | 105 +
src/pages/SSL/components/Step2/index.tsx | 33 +
src/pages/SSL/locales/en-US.ts | 53 +
src/pages/SSL/locales/zh-CN.ts | 53 +
src/pages/SSL/service.ts | 76 +
src/pages/SSL/style.less | 117 +
src/pages/SSL/typing.d.ts | 68 +
src/pages/Setting/Setting.tsx | 175 +-
src/pages/Setting/index.ts | 16 +
src/pages/Setting/locales/en-US.ts | 25 +
src/pages/Setting/locales/zh-CN.ts | 23 +
src/pages/Setting/service.ts | 28 +-
src/pages/Setting/style.less | 16 +
src/pages/Setting/typingd.d.ts | 20 +-
src/pages/Upstream/Create.tsx | 89 +
src/pages/Upstream/List.tsx | 113 +
src/pages/Upstream/components/Preview.tsx | 28 +
src/pages/Upstream/components/Step1.tsx | 188 +
src/pages/Upstream/constants.ts | 31 +
src/pages/Upstream/index.ts | 18 +
src/pages/Upstream/locales/en-US.ts | 62 +
src/pages/Upstream/locales/zh-CN.ts | 62 +
src/pages/Upstream/service.ts | 45 +
src/pages/Upstream/transform.ts | 51 +
src/pages/Upstream/typing.d.ts | 49 +
src/pages/User/Login.less | 132 +
src/pages/User/Login.tsx | 123 +
src/pages/User/Logout.tsx | 44 +
.../User/components/LoginMethodExample.tsx | 43 +
.../User/components/LoginMethodPassword.tsx | 134 +
src/pages/User/index.ts | 2 +
src/pages/User/locales/en-US.ts | 14 +
src/pages/User/locales/zh-CN.ts | 13 +
src/pages/User/typing.d.ts | 25 +
src/pages/document.ejs | 25 +-
src/pages/ssl/Create.tsx | 65 -
src/pages/ssl/List.tsx | 110 -
.../ssl/components/CertificateForm/index.tsx | 75 -
src/pages/ssl/components/Step1/index.tsx | 104 -
src/pages/ssl/components/Step2/index.tsx | 32 -
src/pages/ssl/components/Step3/index.tsx | 36 -
src/pages/ssl/service.ts | 78 -
src/pages/ssl/typing.d.ts | 18 -
src/service-worker.js | 23 +
src/services/API.d.ts | 16 +
src/services/login.ts | 16 +
src/services/user.ts | 33 +-
src/transforms/global.ts | 16 +
src/typings.d.ts | 16 +
tests/PuppeteerEnvironment.js | 16 +
tests/beforeTest.js | 16 +
tests/getBrowser.js | 16 +
tests/run-tests.js | 16 +
yarn.lock | 10533 +++++++---------
279 files changed, 15218 insertions(+), 8859 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE
create mode 100644 .github/PULL_REQUEST_TEMPLATE
create mode 100644 .github/apisix-config.yaml
create mode 100644 .github/workflows/api_ci.yml
create mode 100644 .github/workflows/api_cicd.yml
delete mode 100644 .github/workflows/api_ut.yml
create mode 100644 .github/workflows/ci.yml
rename .github/workflows/{api_cd.yml => deploy.yml} (59%)
create mode 100644 CHANGELOG.md
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 CONTRIBUTING.md
create mode 100644 I18N_USER_GUIDE.md
create mode 100644 LICENSE
create mode 100644 NOTICE
delete mode 100644 README-dashboard.md
create mode 100644 README.zh-CN.md
create mode 100644 USER_GUIDE.md
rename api/{conf.json => conf/conf_preview.json} (54%)
create mode 100644 api/filter/authentication.go
create mode 100644 api/route/authentication.go
create mode 100644 api/route/authentication_test.go
create mode 100644 api/route/base.go
create mode 100644 api/route/base_test.go
create mode 100644 api/route/consumer_test.go
create mode 100644 api/route/ssl_test.go
create mode 100644 api/route/zclear_test.go
create mode 100755 api/run/run.sh
create mode 100644 compose/dashboard_conf/nginx.conf
delete mode 100644 compose/pics/grafana_1.png
delete mode 100644 compose/pics/grafana_2.png
delete mode 100644 compose/pics/grafana_3.png
delete mode 100644 compose/pics/grafana_4.png
delete mode 100644 compose/pics/grafana_5.png
delete mode 100644 compose/pics/grafana_6.png
delete mode 100644 compose/pics/login.png
create mode 100644 images/manager-api.png
create mode 100644 images/metrics-cn.png
create mode 100644 images/metrics-en.png
create mode 100644 images/route-create-done-list-cn.png
create mode 100644 images/route-create-done-list-en.png
create mode 100644 images/route-create-step1-cn.png
create mode 100644 images/route-create-step1-en.png
create mode 100644 images/route-create-step2-cn.png
create mode 100644 images/route-create-step2-en.png
create mode 100644 images/route-create-step3-cn.png
create mode 100644 images/route-create-step3-en.png
create mode 100644 images/route-create-step4-cn.png
create mode 100644 images/route-create-step4-en.png
create mode 100644 images/route-list-en.png
create mode 100644 images/setting-cn.png
create mode 100644 images/setting-en.png
create mode 100644 images/ssl-list-cn.png
create mode 100644 images/ssl-list-en.png
create mode 100644 licenses/LICENSE-ant-design-pro.txt
create mode 100644 licenses/LICENSE-dag-to-lua.txt
create mode 100644 manager-api.md
delete mode 100644 public/home_bg.png
delete mode 100644 public/icons/icon-128x128.png
delete mode 100644 public/icons/icon-192x192.png
delete mode 100644 public/icons/icon-512x512.png
delete mode 100644 public/pro_icon.svg
create mode 100644 src/components/ActionBar/ActionBar.tsx
create mode 100644 src/components/ActionBar/index.ts
create mode 100644 src/components/ActionBar/locales/en-US.ts
create mode 100644 src/components/ActionBar/locales/zh-CN.ts
create mode 100644 src/components/PanelSection/index.tsx
create mode 100644 src/components/PluginPage/PluginCard.tsx
create mode 100644 src/components/PluginPage/PluginDrawer.tsx
create mode 100644 src/components/PluginPage/PluginPage.tsx
create mode 100644 src/components/PluginPage/data.ts
create mode 100644 src/components/PluginPage/index.ts
create mode 100644 src/components/PluginPage/locales/en-US.ts
create mode 100644 src/components/PluginPage/locales/zh-CN.ts
create mode 100644 src/components/PluginPage/service.ts
create mode 100644 src/components/PluginPage/typing.d.ts
create mode 100644 src/constants.ts
create mode 100644 src/helpers.tsx
create mode 100644 src/iconfont.ts
create mode 100644 src/pages/Consumer/Create.tsx
create mode 100644 src/pages/Consumer/List.tsx
create mode 100644 src/pages/Consumer/components/Preview.tsx
create mode 100644 src/pages/Consumer/components/Step1.tsx
create mode 100644 src/pages/Consumer/index.ts
create mode 100644 src/pages/Consumer/locales/en-US.ts
create mode 100644 src/pages/Consumer/locales/zh-CN.ts
create mode 100644 src/pages/Consumer/service.ts
create mode 100644 src/pages/Consumer/typing.d.ts
create mode 100644 src/pages/Metrics/index.ts
create mode 100644 src/pages/Metrics/locales/en-US.ts
create mode 100644 src/pages/Metrics/locales/zh-CN.ts
create mode 100644 src/pages/Metrics/service.ts
rename src/pages/{Routes => Route}/Create.less (67%)
rename src/pages/{Routes => Route}/Create.tsx (50%)
create mode 100644 src/pages/Route/List.tsx
create mode 100644 src/pages/Route/components/CreateStep4/CreateStep4.tsx
create mode 100644 src/pages/Route/components/CreateStep4/index.ts
create mode 100644 src/pages/Route/components/ResultView/ResultView.tsx
create mode 100644 src/pages/Route/components/ResultView/index.ts
create mode 100644 src/pages/Route/components/Step1/MatchingRulesView.tsx
create mode 100644 src/pages/Route/components/Step1/MetaView.tsx
rename src/pages/{Routes => Route}/components/Step1/RequestConfigView.tsx (54%)
rename src/pages/{Routes => Route}/components/Step1/index.tsx (55%)
rename src/pages/{Routes => Route}/components/Step2/HttpHeaderRewriteView.tsx (55%)
create mode 100644 src/pages/Route/components/Step2/RequestRewriteView.tsx
create mode 100644 src/pages/Route/components/Step2/index.tsx
rename src/pages/{Routes => Route}/constants.ts (55%)
create mode 100644 src/pages/Route/index.ts
create mode 100644 src/pages/Route/locales/en-US.ts
create mode 100644 src/pages/Route/locales/zh-CN.ts
create mode 100644 src/pages/Route/service.ts
rename src/pages/{Routes => Route}/transform.ts (65%)
rename src/pages/{Routes => Route}/typing.d.ts (68%)
delete mode 100644 src/pages/Routes/List.tsx
delete mode 100644 src/pages/Routes/components/ActionBar/ActionBar.tsx
delete mode 100644 src/pages/Routes/components/ActionBar/index.ts
delete mode 100644 src/pages/Routes/components/CreateStep3/CreateStep3.tsx
delete mode 100644 src/pages/Routes/components/CreateStep3/PluginCard.tsx
delete mode 100644 src/pages/Routes/components/CreateStep3/PluginDrawer.tsx
delete mode 100644 src/pages/Routes/components/CreateStep3/index.ts
delete mode 100644 src/pages/Routes/components/CreateStep4/CreateStep4.tsx
delete mode 100644 src/pages/Routes/components/CreateStep4/index.ts
delete mode 100644 src/pages/Routes/components/PanelSection/index.tsx
delete mode 100644 src/pages/Routes/components/ResultView/ResultView.tsx
delete mode 100644 src/pages/Routes/components/ResultView/index.ts
delete mode 100644 src/pages/Routes/components/Step1/MatchingRulesView.tsx
delete mode 100644 src/pages/Routes/components/Step1/MetaView.tsx
delete mode 100644 src/pages/Routes/components/Step2/RequestRewriteView.tsx
delete mode 100644 src/pages/Routes/components/Step2/index.tsx
delete mode 100644 src/pages/Routes/service.ts
rename src/pages/{ssl => SSL}/Create.less (64%)
create mode 100644 src/pages/SSL/Create.tsx
create mode 100644 src/pages/SSL/List.tsx
create mode 100644 src/pages/SSL/components/CertificateForm/index.tsx
rename src/pages/{ssl => SSL}/components/CertificateUploader/index.tsx (62%)
create mode 100644 src/pages/SSL/components/Step1/index.tsx
create mode 100644 src/pages/SSL/components/Step2/index.tsx
create mode 100644 src/pages/SSL/locales/en-US.ts
create mode 100644 src/pages/SSL/locales/zh-CN.ts
create mode 100644 src/pages/SSL/service.ts
create mode 100644 src/pages/SSL/style.less
create mode 100644 src/pages/SSL/typing.d.ts
create mode 100644 src/pages/Setting/locales/en-US.ts
create mode 100644 src/pages/Setting/locales/zh-CN.ts
create mode 100644 src/pages/Upstream/Create.tsx
create mode 100644 src/pages/Upstream/List.tsx
create mode 100644 src/pages/Upstream/components/Preview.tsx
create mode 100644 src/pages/Upstream/components/Step1.tsx
create mode 100644 src/pages/Upstream/constants.ts
create mode 100644 src/pages/Upstream/index.ts
create mode 100644 src/pages/Upstream/locales/en-US.ts
create mode 100644 src/pages/Upstream/locales/zh-CN.ts
create mode 100644 src/pages/Upstream/service.ts
create mode 100644 src/pages/Upstream/transform.ts
create mode 100644 src/pages/Upstream/typing.d.ts
create mode 100644 src/pages/User/Login.less
create mode 100644 src/pages/User/Login.tsx
create mode 100644 src/pages/User/Logout.tsx
create mode 100644 src/pages/User/components/LoginMethodExample.tsx
create mode 100644 src/pages/User/components/LoginMethodPassword.tsx
create mode 100644 src/pages/User/index.ts
create mode 100644 src/pages/User/locales/en-US.ts
create mode 100644 src/pages/User/locales/zh-CN.ts
create mode 100644 src/pages/User/typing.d.ts
delete mode 100644 src/pages/ssl/Create.tsx
delete mode 100644 src/pages/ssl/List.tsx
delete mode 100644 src/pages/ssl/components/CertificateForm/index.tsx
delete mode 100644 src/pages/ssl/components/Step1/index.tsx
delete mode 100644 src/pages/ssl/components/Step2/index.tsx
delete mode 100644 src/pages/ssl/components/Step3/index.tsx
delete mode 100644 src/pages/ssl/service.ts
delete mode 100644 src/pages/ssl/typing.d.ts
diff --git a/.eslintrc.js b/.eslintrc.js
index b882c20e87..f8549393c8 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,3 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: {
diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE
new file mode 100644
index 0000000000..be81252da4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE
@@ -0,0 +1,23 @@
+Please answer these questions before submitting your issue.
+
+- Why do you submit this issue?
+- [ ] Question or discussion
+- [ ] Bug
+- [ ] Requirements
+- [ ] Feature or performance improvement
+- [ ] Other
+
+___
+### Question
+- What do you want to know?
+
+___
+### Bug
+- Which version of Apache APISIX Dashboard, OS and Broswer?
+
+- What happened?
+If possible, provide a way to reproduce the error.
+
+___
+### Requirement or improvement
+- Please describe your requirements or improvement suggestions.
diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE
new file mode 100644
index 0000000000..d4051d9875
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE
@@ -0,0 +1,18 @@
+Please answer these questions before submitting a pull request
+
+- Why submit this pull request?
+- [ ] Bug fix
+- [ ] New feature provided
+- [ ] Improve performance
+
+- Related issues
+
+___
+### Bugfix
+- Description
+
+- How to fix?
+
+___
+### New feature or improvement
+- Describe the details and related test reports.
diff --git a/.github/apisix-config.yaml b/.github/apisix-config.yaml
new file mode 100644
index 0000000000..2ff8ceba94
--- /dev/null
+++ b/.github/apisix-config.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# If you want to set the specified configuration value, you can set the new
+# in this file. For example if you want to specify the etcd address:
+#
+
+etcd:
+ host:
+ - "http://etcd:2379"
+
+apisix:
+ allow_admin: # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow
+ - 0.0.0.0/0 # If we don't set any IP list, then any IP access is allowed by default.
diff --git a/.github/workflows/api_ci.yml b/.github/workflows/api_ci.yml
new file mode 100644
index 0000000000..039f26cfc1
--- /dev/null
+++ b/.github/workflows/api_ci.yml
@@ -0,0 +1,76 @@
+name: API CI
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+
+ run-test:
+
+ runs-on: ubuntu-latest
+
+ services:
+ etcd:
+ image: bitnami/etcd:3.3.13-r80
+ ports:
+ - 2379:2379
+ - 2380:2380
+ env:
+ ALLOW_NONE_AUTHENTICATION: yes
+
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: 123456
+ ports:
+ - '3306:3306'
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+
+ steps:
+
+ - uses: actions/checkout@v2
+
+ - name: run apisix
+ run: |
+ network=$(docker network ls | grep github_network | awk '{print $2}')
+ docker run --name apisix -d -p 9080:9080 \
+ -v ${{ github.workspace }}/.github/apisix-config.yaml:/usr/local/apisix/conf/config.yaml \
+ --network "$network" --network-alias apisix \
+ apache/apisix:dev
+ sleep 5
+ docker logs apisix
+
+ - name: setting up database
+ run: |
+ mysql -h 127.0.0.1 --port 3306 -u root -p123456 < ./api/script/db/schema.sql
+
+ - name: ping apisix
+ run: |
+ curl 127.0.0.1:9080
+
+ - name: get lua lib
+ run: |
+ wget https://github.com/api7/dag-to-lua/archive/v1.0.tar.gz
+ sudo mkdir -p /go/api7-manager-api/dag-to-lua/
+ tar -zxvf v1.0.tar.gz
+ sudo mv ./dag-to-lua-1.0/lib/* /go/api7-manager-api/dag-to-lua/
+
+ - name: install runtime
+ run: |
+ sudo apt-get update
+ sudo apt-get install lua5.1
+ sudo add-apt-repository ppa:longsleep/golang-backports
+ sudo apt update
+ export GO111MOUDULE=on
+ sudo apt install golang-1.14-go
+
+ - name: run test
+ working-directory: ./api
+ run: |
+ go test ./...
diff --git a/.github/workflows/api_cicd.yml b/.github/workflows/api_cicd.yml
new file mode 100644
index 0000000000..43cb2ae190
--- /dev/null
+++ b/.github/workflows/api_cicd.yml
@@ -0,0 +1,77 @@
+name: API CI && CD
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ services:
+ etcd:
+ image: bitnami/etcd:3.3.13-r80
+ ports:
+ - 2379:2379
+ - 2380:2380
+ env:
+ ALLOW_NONE_AUTHENTICATION: yes
+
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: 123456
+ ports:
+ - '3306:3306'
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: run apisix
+ run: |
+ network=$(docker network ls | grep github_network | awk '{print $2}')
+ docker run --name apisix -d -p 9080:9080 \
+ -v ${{ github.workspace }}/.github/apisix-config.yaml:/usr/local/apisix/conf/config.yaml \
+ --network "$network" --network-alias apisix \
+ apache/apisix:dev
+ sleep 5
+ docker logs apisix
+
+ - name: setting up database
+ run: |
+ mysql -h 127.0.0.1 --port 3306 -u root -p123456 < ./api/script/db/schema.sql
+
+ - name: ping apisix
+ run: |
+ curl 127.0.0.1:9080
+
+ - name: get lua lib
+ run: |
+ wget https://github.com/api7/dag-to-lua/archive/v1.0.tar.gz
+ sudo mkdir -p /go/api7-manager-api/dag-to-lua/
+ tar -zxvf v1.0.tar.gz
+ sudo mv ./dag-to-lua-1.0/lib/* /go/api7-manager-api/dag-to-lua/
+
+ - name: install runtime
+ run: |
+ sudo apt-get update
+ sudo apt-get install lua5.1
+ sudo add-apt-repository ppa:longsleep/golang-backports
+ sudo apt update
+ export GO111MOUDULE=on
+ sudo apt install golang-1.14-go
+
+ - uses: Azure/docker-login@v1
+ with:
+ login-server: apisixacr.azurecr.cn
+ username: ${{ secrets.REGISTRY_USERNAME }}
+ password: ${{ secrets.REGISTRY_PASSWORD }}
+
+ - name: build and push docker image
+ run: |
+ cd ./api
+ docker build . -t apisixacr.azurecr.cn/managerapi:${{ github.sha }}
+ docker push apisixacr.azurecr.cn/managerapi:${{ github.sha }}
diff --git a/.github/workflows/api_ut.yml b/.github/workflows/api_ut.yml
deleted file mode 100644
index 2b3c4bb4e2..0000000000
--- a/.github/workflows/api_ut.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: API unit test
-
-on:
- push:
- branches:
- - master
- - manager
- pull_request:
- branches:
- - master
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: use docker-compose in api
- run: cd ./api && docker-compose up
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000000..138d78b7fa
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
+# This is a basic test to build the dashboard
+
+name: CI
+
+# Controls when the action will run. Triggers the workflow on push or pull request
+# events but only for the master branch
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ # This workflow contains a single job called "build"
+ build:
+ # The type of runner that the job will run on
+ runs-on: ubuntu-latest
+
+ # Steps represent a sequence of tasks that will be executed as part of the job
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v2.1.1
+
+ # Install packages
+ - name: Install packages
+ run: yarn
+
+ # Build the dashboard
+ - name: Build the application
+ run: yarn build
diff --git a/.github/workflows/api_cd.yml b/.github/workflows/deploy.yml
similarity index 59%
rename from .github/workflows/api_cd.yml
rename to .github/workflows/deploy.yml
index a2e0cfbb4a..86cc48c74f 100644
--- a/.github/workflows/api_cd.yml
+++ b/.github/workflows/deploy.yml
@@ -1,16 +1,17 @@
-name: API CD
+name: Deploy to Azure
on:
push:
- branches:
- - master
+ branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
+
steps:
+
- uses: actions/checkout@v2
-
+
- uses: Azure/docker-login@v1
with:
login-server: apisixacr.azurecr.cn
@@ -18,6 +19,5 @@ jobs:
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
- cd ./api
- docker build . -t apisixacr.azurecr.cn/managerapi:${{ github.sha }}
- docker push apisixacr.azurecr.cn/managerapi:${{ github.sha }}
+ docker build . -t apisixacr.azurecr.cn/dashboard:${{ github.sha }}
+ docker push apisixacr.azurecr.cn/dashboard:${{ github.sha }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9caa19201b..571eb5b696 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,4 +40,7 @@ build
/compose/**/*.log
/compose/**/nginx.pid
-/compose/etcd_data
\ No newline at end of file
+/compose/etcd_data
+manager-api
+conf.json
+conf.json-e
diff --git a/.prettierrc.js b/.prettierrc.js
index 7b597d7891..64d2db1003 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -1,3 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
const fabric = require('@umijs/fabric');
module.exports = {
diff --git a/.stylelintrc.js b/.stylelintrc.js
index c2030787de..22ebf981d1 100644
--- a/.stylelintrc.js
+++ b/.stylelintrc.js
@@ -1,3 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
const fabric = require('@umijs/fabric');
module.exports = {
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..45535262cf
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,47 @@
+
+
+# Table of Contents
+
+- [1.5.0](#150)
+- [1.0.0](#100)
+
+# 1.5.0
+
+This release mainly refactors the dashboard.
+
+### Core
+
+- Integrate with Ant Design Pro. [#263](https://github.com/apache/apisix-dashboard/pull/263)
+- Added Manager API support to process logics between APISIX and Dashboard.
+- Added Metrics/Route/SSL/Upstream/Consumer module.
+
+## 1.0.0
+
+This release is mainly to build some basic panels and resolve License issue.
+
+### Core
+
+- Dashboard initial. [#1](https://github.com/apache/apisix-dashboard/pull/1)
+- Resolve licence issues.
+- Remove unused files from the Dashboard boilerplate.
+- Support panel to list, create and modify Route, Consumer, Service, SSL and Upstream.
+- Support custom configuration for Plugin dialog.
+
+[Back to TOC](#table-of-contents)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..573c48b8c9
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,105 @@
+
+
+_The following is copied for your convenience from . If there's a discrepancy between the two, let us know or submit a PR to fix it._
+
+# Code of Conduct
+
+## Introduction
+
+This code of conduct applies to all spaces managed by the Apache Software Foundation, including IRC, all public and private mailing lists, issue trackers, wikis, blogs, Twitter, and any other communication channel used by our communities. A code of conduct which is specific to in-person events (ie., conferences) is codified in the published ASF anti-harassment policy.
+
+We expect this code of conduct to be honored by everyone who participates in the Apache community formally or informally, or claims any affiliation with the Foundation, in any Foundation-related activities and especially when representing the ASF, in any role.
+
+This code **is not exhaustive or complete**. It serves to distill our common understanding of a collaborative, shared environment and goals. We expect it to be followed in spirit as much as in the letter, so that it can enrich all of us and the technical communities in which we participate.
+
+## Specific Guidelines
+
+We strive to:
+
+1. **Be open.** We invite anyone to participate in our community. We preferably use public methods of communication for project-related messages, unless discussing something sensitive. This applies to messages for help or project-related support, too; not only is a public support request much more likely to result in an answer to a question, it also makes sure that any inadvertent mistakes made by people answering will be more easily detected and corrected.
+
+2. **Be `empathetic`, welcoming, friendly, and patient.** We work together to resolve conflict, assume good intentions, and do our best to act in an empathetic fashion. We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal attack. A community where people feel uncomfortable or threatened is not a productive one. We should be respectful when dealing with other community members as well as with people outside our community.
+
+3. **Be collaborative.** Our work will be used by other people, and in turn we will depend on the work of others. When we make something for the benefit of the project, we are willing to explain to others how it works, so that they can build on the work to make it even better. Any decision we make will affect users and colleagues, and we take those consequences seriously when making decisions.
+
+4. **Be inquisitive.** Nobody knows everything! Asking questions early avoids many problems later, so questions are encouraged, though they may be directed to the appropriate forum. Those who are asked should be responsive and helpful, within the context of our shared goal of improving Apache project code.
+
+5. **Be careful in the words that we choose.** Whether we are participating as professionals or volunteers, we value professionalism in all interactions, and take responsibility for our own speech. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behaviour are not acceptable. This includes, but is not limited to:
+
+ - Violent threats or language directed against another person.
+ - Sexist, racist, or otherwise discriminatory jokes and language.
+ - Posting sexually explicit or violent material.
+ - Posting (or threatening to post) other people's personally identifying information ("doxing").
+ - Sharing private content, such as emails sent privately or non-publicly, or unlogged forums such as IRC channel history.
+ - Personal insults, especially those using racist or sexist terms.
+ - Unwelcome sexual attention.
+ - Excessive or unnecessary profanity.
+ - Repeated harassment of others. In general, if someone asks you to stop, then stop.
+ - Advocating for, or encouraging, any of the above behaviour.
+
+6. **Be concise.** Keep in mind that what you write once will be read by hundreds of persons. Writing a short email means people can understand the conversation as efficiently as possible. Short emails should always strive to be empathetic, welcoming, friendly and patient. When a long explanation is necessary, consider adding a summary.
+
+ Try to bring new ideas to a conversation so that each mail adds something unique to the thread, keeping in mind that the rest of the thread still contains the other messages with arguments that have already been made.
+
+ Try to stay on topic, especially in discussions that are already fairly large.
+
+7. **Step down considerately.** Members of every project come and go. When somebody leaves or disengages from the project they should tell people they are leaving and take the proper steps to ensure that others can pick up where they left off. In doing so, they should remain respectful of those who continue to participate in the project and should not misrepresent the project's goals or achievements. Likewise, community members should respect any individual's choice to leave the project.
+
+## Diversity Statement
+
+Apache welcomes and encourages participation by everyone. We are committed to being a community that everyone feels good about joining. Although we may not be able to satisfy everyone, we will always work to treat everyone well.
+
+No matter how you identify yourself or how others perceive you: we welcome you. Though no list can hope to be comprehensive, we explicitly honour diversity in: age, culture, ethnicity, genotype, gender identity or expression, language, national origin, neurotype, phenotype, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, subculture and technical ability.
+
+Though we welcome people fluent in all languages, Apache development is conducted in English.
+
+Standards for behaviour in the Apache community are detailed in the Code of Conduct above. We expect participants in our community to meet these standards in all their interactions and to help others to do so as well.
+
+## Reporting Guidelines
+
+While this code of conduct should be adhered to by participants, we recognize that sometimes people may have a bad day, or be unaware of some of the guidelines in this code of conduct. When that happens, you may reply to them and point out this code of conduct. Such messages may be in public or in private, whatever is most appropriate. However, regardless of whether the message is public or not, it should still adhere to the relevant parts of this code of conduct; in particular, it should not be abusive or disrespectful.
+
+If you believe someone is violating this code of conduct, you may reply to them and point out this code of conduct. Such messages may be in public or in private, whatever is most appropriate. Assume good faith; it is more likely that participants are unaware of their bad behaviour than that they intentionally try to degrade the quality of the discussion. Should there be difficulties in dealing with the situation, you may report your compliance issues in confidence to either:
+
+- President of the Apache Software Foundation: Sam Ruby (rubys at intertwingly dot net)
+
+or one of our volunteers:
+
+- [Mark Thomas](http://home.apache.org/~markt/coc.html)
+- [Joan Touzet](http://home.apache.org/~wohali/)
+- [Sharan Foga](http://home.apache.org/~sharan/coc.html)
+
+If the violation is in documentation or code, for example inappropriate pronoun usage or word choice within official documentation, we ask that people report these privately to the project in question at private@project.apache.org, and, if they have sufficient ability within the project, to resolve or remove the concerning material, being mindful of the perspective of the person originally reporting the issue.
+
+## End Notes
+
+This Code defines **empathy** as "a vicarious participation in the emotions, ideas, or opinions of others; the ability to imagine oneself in the condition or predicament of another." **Empathetic** is the adjectival form of empathy.
+
+This statement thanks the following, on which it draws for content and inspiration:
+
+- [CouchDB Project Code of conduct](http://couchdb.apache.org/conduct.html)
+- [Fedora Project Code of Conduct](http://fedoraproject.org/code-of-conduct)
+- [Speak Up! Code of Conduct](http://speakup.io/coc.html)
+- [Django Code of Conduct](https://www.djangoproject.com/conduct/)
+- [Debian Code of Conduct](http://www.debian.org/vote/2014/vote_002)
+- [Twitter Open Source Code of Conduct](https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md)
+- [Mozilla Code of Conduct/Draft](https://wiki.mozilla.org/Code_of_Conduct/Draft#Conflicts_of_Interest)
+- [Python Diversity Appendix](https://www.python.org/community/diversity/)
+- [Python Mentors Home Page](http://pythonmentors.com/)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..b84ca1586a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Contributing to Apache APISIX Dashboard
+
+Firstly, thanks for your interest in contributing! I hope that this will be a
+pleasant first experience for you, and that you will return to continue
+contributing.
+
+## Code of Conduct
+
+This project and everyone participating in it is governed by the Apache
+software Foundation's
+[Code of Conduct](http://www.apache.org/foundation/policies/conduct.html). By
+participating, you are expected to adhere to this code. If you are aware of
+unacceptable behavior, please visit the
+[Reporting Guidelines page](http://www.apache.org/foundation/policies/conduct.html#reporting-guidelines)
+and follow the instructions there.
+
+## How to contribute?
+
+Most of the contributions that we receive are code contributions, but you can
+also contribute to the documentation or simply report solid bugs
+for us to fix.
+
+## How to report a bug?
+
+* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/apache/apisix-dashboard/issues).
+
+* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/apache/apisix-dashboard/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
+
+
+## How to add a new feature or change an existing one
+
+_Before making any significant changes, please [open an issue](https://github.com/apache/apisix-dashboard/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone.
+
+Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request. Your PR is most likely to be accepted if it:
+
+* Update the README.md with details of changes to the interface.
+* Includes tests for new functionality.
+* References the original issue in description, e.g. "resolve #123".
+* Has a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
+
+## Do you have questions about the source code?
+
+* Subscribe to our mail list and send the question mail to [dev@apisix.apache.org](mailto:dev@apisix.apache.org)
diff --git a/Dockerfile b/Dockerfile
index 72ac8d3dbb..90d4fde5ea 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,37 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
# phase-build
-FROM node:12-alpine as builder
+FROM node:12 as builder
WORKDIR /usr/src/app/
USER root
-COPY package.json /usr/src/app/
+COPY package.json ./
+COPY yarn.lock ./
RUN yarn
-COPY . /usr/src/app/
+COPY ./ ./
RUN yarn build && rm -rf /usr/src/app/node_modules
# phase-run
FROM nginx:1.16-alpine
COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf
-COPY --from=builder /usr/src/app/dist /usr/share/nginx/html/dashboard
+COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
EXPOSE 80
diff --git a/I18N_USER_GUIDE.md b/I18N_USER_GUIDE.md
new file mode 100644
index 0000000000..0ebd44f6d0
--- /dev/null
+++ b/I18N_USER_GUIDE.md
@@ -0,0 +1,147 @@
+
+
+# Apache APISIX Dashboard I18N User Guide
+
+The Apache APISIX Dashboard uses [@umijs/plugin-locale](https://umijs.org/plugins/plugin-locale) to solve the i18n issues, in order to make the i18n more clear and reasonable, we would recommend to obey the following rules
+
+## Location of locale configuration:
+
+- Please put **the global locales** under `src/locales`.
+- Please put **each page's locale file** under `src/pages/$PAGE/locales` folder.
+- Please put **the Component's locale file** under `src/components/$COMPONENT/locales` folder, and we **MUST** import them manually
+
+## How to name the key for each locale filed:
+
+the key can be like this : [basicModule].[moduleName].[elementName].[...desc]
+
+- what's the first tow levels? e.g: `app.pwa`, `page.consumer`, `component.actionBar`
+
+- The subkeys are divided into $element + $description style e.g: `app.pwa.message.offline`, `component.actionBar.button.nextStep`
+
+ - If the the text is the part of a element, we can use [elementNameProps] e.g: `page.consumer.proTableColumns.username`.
+ - If there are two or more same level part locales of a element, we can add number suffix e.g: `page.route.form.itemRulesExtraMessage1.path`, `page.route.form.itemRulesExtraMessage2.path`.
+
+- common texts, we should not repeat in other part, and the common locale key omit [elementName] would be better.
+
+ - If the text is used in two or more places inside the module, we would recommend sharing the text in the module, e.g:`page.route.parameterPosition`.
+ - If the text is used in two or more places between modules, we would recommend sharing the text globally, and add`global`as the moduleName,git e.g:`component.global.confirm`.
+
+## Global locale keys
+
+we have already defined many global keys, before you do i18n, you can refer to [those](https://github.com/apache/apisix-dashboard/blob/master/src/locales/zh-CN/component.ts).
+
+## Recommended subkey naming
+
+- **Form**
+
+| element | props | locale subKey |
+| --------- | -------------- | ----------------------------- |
+| Form.Item | label | form.itemLabel |
+| Form.Item | rules.required | form.itemRulesRequiredMessage |
+| Form.Item | rules.pattern | form.itemRulesPatternMessage |
+| Form.Item | extra | form.itemExtraMessage |
+
+**Example:**
+
+```js
+'page.route.form.itemRulesExtraMessage.parameterName': '仅支持字母和数字,且只能以字母开头',
+'page.route.form.itemLabel.apiName': 'API 名称',
+'page.route.form.itemRulesPatternMessage.apiNameRule': '最大长度100,仅支持字母、数字、- 和 _,且只能以字母开头',
+```
+
+- **Input**
+
+| element | props | locale subKey |
+| ------- | ----------- | ----------------- |
+| Input | placeholder | input.placeholder |
+
+**Example:**
+
+```js
+'page.route.input.placeholder.parameterNameHttpHeader': '请求头键名,例如:HOST',
+```
+
+- **Button**
+
+| element | props | locale subKey |
+| ------- | ----- | ------------- |
+| Button | null | button |
+
+**Example:**
+
+```js
+'page.route.button.returnList': '返回路由列表',
+```
+
+- **PanelSection**
+
+| element | props | locale subKey |
+| ------------ | ----- | ------------------ |
+| PanelSection | title | panelSection.title |
+
+**Example:**
+
+```js
+'page.route.panelSection.title.nameDescription': '名称及其描述',
+```
+
+- **Steps**
+
+| element | props | locale subKey |
+| ---------- | ----- | --------------- |
+| Steps.step | title | steps.stepTitle |
+
+**Example:**
+
+```js
+'page.route.steps.stepTitle.defineApiRequest': '定义 API 请求',
+```
+
+- **Select**
+
+| element | props | locale subKey |
+| ------------- | ----- | ------------- |
+| Select.Option | null | select.option |
+
+**Example:**
+
+```js
+'page.route.select.option.enableHttps': '启用 HTTPS',
+```
+
+- **Radio**
+
+| element | props | locale subKey |
+| ------- | ----- | ------------- |
+| Radio | null | radio |
+
+**Example:**
+
+```js
+'page.route.radio.staySame': '保持原样',
+```
+
+- **ProTable**
+
+| element | props | locale subKey |
+| -------- | ------------- | --------------------- |
+| ProTable | columns.title | proTable.columnsTitle |
+
+_ProTable usually appears in conjunction with forms, and columns title are same with form item label, so we recommend these title keys to be the common key in modules._
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..46a8d39f21
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,227 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+=======================================================================
+Apache APISIX Dashboard Subcomponents:
+
+The Apache APISIX Dashboard project contains subcomponents with separate copyright
+notices and license terms. Your use of the source code for the these
+subcomponents is subject to the terms and conditions of the following
+licenses.
+
+========================================================================
+MIT licenses
+========================================================================
+
+The following components are provided under the MIT License. See project link for details.
+The text of each license is also included at licenses/LICENSE-[project].txt.
+
+ files from ant-design-pro: https://github.com/ant-design/ant-design-pro MIT
+
+========================================================================
+Apache 2.0 licenses
+========================================================================
+
+The following components are provided under the Apache 2.0 License. See project link for details.
+The text of each license is also included at licenses/LICENSE-[project].txt.
+
+ files from dag-to-lua: https://github.com/api7/dag-to-lua Apache 2.0
\ No newline at end of file
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000000..ba0415537e
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Apache APISIX
+Copyright 2019-2020 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
\ No newline at end of file
diff --git a/README-dashboard.md b/README-dashboard.md
deleted file mode 100644
index 74f87484ed..0000000000
--- a/README-dashboard.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# READMD for Dashboard
-
-This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
-
-## Environment Prepare
-
-1. Make sure you have `Node.js` installed on your machine.
-2. Install [yarn](https://yarnpkg.com/).
-3. Install `node_modules`:
-
-```bash
-$ yarn
-```
-
-### Start project
-
-```bash
-yarn start:no-mock
-```
-
-### Build project
-
-```bash
-yarn build
-```
-
-### Check code style
-
-```bash
-yarn lint
-```
-
-You can also use script to auto fix some lint error:
-
-```bash
-yarn lint:fix
-```
-
-### Test code
-
-```bash
-yarn test
-```
-
-## More
-
-You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).
diff --git a/README.md b/README.md
index 97b8f0b43e..82eadd00e7 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,117 @@
+
+English | [简体中文](./README.zh-CN.md)
+
# Apache APISIX Dashboard
-Dashboard for [Apache APISIX](https://github.com/apache/incubator-apisix-dashboard)
+Dashboard for [Apache APISIX](https://github.com/apache/apisix)
+
+[Online demo](http://139.217.190.60/)
+
+## User Guide
+Please refer to [User Guide](./USER_GUIDE.md)
-## Deploy with Docker (currently)
+## Deploy with Docker
Please refer to [Deploy with Docker README](./compose/README.md)
-## More
+## Deploy Manually
+
+### Clone the project
+
+```sh
+$ git clone https://github.com/apache/apisix-dashboard.git
+
+$ cd apisix-dashboard
+```
+
+### Build the manager-api
+
+The `manager-api` is used to provide APIs for Dashboard, just like a bridge between the Apache APISIX and the Dashboard. Here are the steps to build it manually:
+
+1. We need `MySQL/Golang` to be preinstalled.
+
+```sh
+# e.g Initialization for MySQL, please use a more secure Password instead of 123456.
+$ mysql –uroot –p123456
+> source ./api/script/db/schema.sql
+```
+
+2. Start the Apache APISIX.
+
+[Please follow this guide](https://github.com/apache/apisix#configure-and-installation)
+
+3. Check environment variables
+
+According to your local deployment environment, check the environment variables in `./api/run/run.sh`, modify the environment variables if needed.
+
+For most users in China, we could use [Goproxy](https://goproxy.cn/) to speed up downloading modules.
+
+4. Build
+
+```sh
+$ cd api && go build -o ../manager-api . && cd ..
+```
+
+5. Run
+
+```sh
+$ sh ./api/run/run.sh &
+```
+
+### Build the Dashboard
+
+This project is initialized with [Ant Design Pro](https://pro.ant.design). The following are some quick guides for how to use.
+
+1. Make sure you have `Node.js(version 8.10.0+)/Nginx` installed on your machine.
+2. Install [yarn](https://yarnpkg.com/).
+3. Install dependencies:
+
+```sh
+$ yarn install
+```
+
+4. Build
+
+```sh
+$ yarn build
+```
+
+5. The bundled files are under `/dist` folder if the step 4 is successful, then we recommend using `nginx` to handle those files, please install `nginx` manually, then refer to the nginx conf `compose/dashboard_conf/nginx.conf`.
+6. Move files under `dist` folder to nginx's default html folder, then visit `http://127.0.0.1` in your browser.
+
+## Development
+
+1. Make sure you have `Node.js(version 8.10.0+)/Nginx` installed on your machine.
+2. Install [yarn](https://yarnpkg.com/).
+3. Install dependencies:
+4. If we want to modify the API, please refer to the `config/proxy.ts` file.
+
+```sh
+$ yarn install
+
+$ yarn start
+```
+
+## Other
+
+1. If you need the dashboard-1.0 which is built with Vue.js, please refer to [master-vue](https://github.com/apache/apisix-dashboard/tree/master-vue).
+
+2. More information about the new dashboard and manager-api please refer to [here](./manager-api.md)
-1. More infomation about the frontend Dashboard, please refer to [README for Dashboard](./README-dashboard.md)
-2. If you need the dashboard built with Vue.js, please refer to [master-vue](https://github.com/apache/incubator-apisix-dashboard/tree/master-vue).
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 0000000000..755fccb49e
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,128 @@
+
+
+[English](./README.md) | 简体中文
+
+# Apache APISIX 仪表盘
+
+[Apache APISIX](https://github.com/apache/apisix-dashboard) 的仪表盘
+
+[在线演示](http://139.217.190.60/)
+
+## 用户指南
+
+请参考 [用户指南](./USER_GUIDE.md)
+
+## 使用 Docker 部署
+
+请参考 [使用 Docker 部署](./compose/README.md)
+
+## 手动部署
+
+### 克隆项目
+
+```sh
+$ git clone https://github.com/apache/apisix-dashboard.git
+
+$ cd apisix-dashboard
+```
+
+### 生成 manager-api
+
+`manager-api` 用于为仪表盘提供接口,就像 Apache APISIX 和仪表盘之间的桥梁。下面是手动构建步骤:
+
+1. 需要预先安装 `MySQL/Golang`。
+
+```sh
+# 例如:初始化时,推荐使用更加安全的密码,而不是 123456
+$ mysql –uroot –p123456
+> source ./api/script/db/schema.sql
+```
+
+2. 启动 Apache APISIX
+
+[请参考这份指南](https://github.com/apache/apisix#configure-and-installation)
+
+3. 检查环境变量
+
+根据您的本地部署环境,检查 `./api/run/run.sh` 中的环境变量,如果需要请修改环境变量。
+
+对于大多数中国用户,我们可以使用 [Goproxy](https://goproxy.cn/) 加快模块下载速度。
+
+4. 构建
+
+```sh
+$ cd api && go build -o ../manager-api . && cd ..
+```
+
+5. 启动
+
+```sh
+$ sh ./api/run/run.sh &
+```
+
+### 构建仪表盘
+
+该项目使用 [Ant Design Pro](https://pro.ant.design) 初始化。以下是一些使用方法的快速指南。
+
+1. 确保你的设备已经安装了 `Node.js(version 8.10.0+)/Nginx`。
+
+2. 安装 [yarn](https://yarnpkg.com/)。
+
+3. 安装依赖:
+
+```sh
+$ yarn install
+```
+
+4. 构建
+
+```sh
+$ yarn build
+```
+
+5. 如果第 4 步成功的话,那么构建后的文件在 `/dist` 目录下,接着我们推荐使用 `nginx` 处理这些文件,请手动安装 `nginx` 并参考 `compose/dashboard_conf/nginx.conf` 配置。
+
+6. 移动 `dist` 目录下的文件到 nginx 的默认 html 目录,然后在浏览器中访问 `http://127.0.0.1`。
+
+## 开发
+
+1. 确保你的设备已经安装了 `Node.js(version 8.10.0+)/Nginx`。
+
+2. 安装 [yarn](https://yarnpkg.com/)。
+
+3. 安装依赖:
+
+```sh
+$ yarn install
+```
+
+4. 如果我们想要修改 API,请参考 `config/proxy.ts` 文件。
+
+```sh
+$ yarn install
+
+$ yarn start
+```
+
+## 其他
+
+1. 如果你需要 Vue.js 构建的 dashboard-1.0,请参考 [master-vue](https://github.com/apache/apisix-dashboard/tree/master-vue)。
+
+2. 关于新版仪表盘和 manager-api 的更多信息请参阅 [这里](./manager-api.md)
diff --git a/USER_GUIDE.md b/USER_GUIDE.md
new file mode 100644
index 0000000000..659850fdac
--- /dev/null
+++ b/USER_GUIDE.md
@@ -0,0 +1,33 @@
+# User Guide
+
+Please visit [http://139.217.190.60/](http://139.217.190.60/) in browser to have a full-preview of the Apache APISIX Dashboard.
+
+The following are parts of the modules' snapshot.
+
+## Metrics
+
+
+
+## Route
+
+The Route module aims to control routes by UI instead of calling APIs.
+
+### List
+
+
+
+### Create
+
+
+
+
+
+
+
+
+
+
+
+## Setting
+
+
diff --git a/api/Dockerfile b/api/Dockerfile
index 85092a15a6..c9e52badb3 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -19,20 +19,25 @@ FROM golang:1.13.8 AS build-env
WORKDIR /go/src/github.com/apisix/manager-api
COPY . .
-RUN mkdir /root/manager-api \
+RUN mkdir /go/manager-api \
&& go env -w GOPROXY=https://goproxy.io,direct \
&& export GOPROXY=https://goproxy.io \
- && go build -o /root/manager-api/manager-api \
- && mv /go/src/github.com/apisix/manager-api/build.sh /root/manager-api/ \
- && mv /go/src/github.com/apisix/manager-api/conf.json /root/manager-api/ \
+ && go build -o /go/manager-api/manager-api \
+ && mv /go/src/github.com/apisix/manager-api/build.sh /go/manager-api/ \
+ && mv /go/src/github.com/apisix/manager-api/conf/conf_preview.json /go/manager-api/conf.json \
&& rm -rf /go/src/github.com/apisix/manager-api \
&& rm -rf /etc/localtime \
&& ln -s /usr/share/zoneinfo/Hongkong /etc/localtime \
&& dpkg-reconfigure -f noninteractive tzdata
+RUN wget https://github.com/api7/dag-to-lua/archive/v1.0.tar.gz \
+ && tar -zxvf v1.0.tar.gz \
+ && mkdir /go/manager-api/dag-to-lua \
+ && mv ./dag-to-lua-1.0/lib/* /go/manager-api/dag-to-lua/
+
FROM alpine:3.11
-RUN mkdir /root/manager-api \
+RUN mkdir -p /go/manager-api \
&& apk update \
&& apk add ca-certificates \
&& update-ca-certificates \
@@ -40,10 +45,11 @@ RUN mkdir /root/manager-api \
&& echo "hosts: files dns" > /etc/nsswitch.conf \
&& rm -rf /var/cache/apk/*
+RUN apk add lua5.1
-WORKDIR /root/manager-api
-COPY --from=build-env /root/manager-api/* /root/manager-api/
+WORKDIR /go/manager-api
+COPY --from=build-env /go/manager-api/ /go/manager-api/
COPY --from=build-env /usr/share/zoneinfo/Hongkong /etc/localtime
EXPOSE 8080
RUN chmod +x ./build.sh
-CMD ["/root/manager-api/build.sh"]
+CMD ["/bin/ash", "-c", "/go/manager-api/build.sh"]
diff --git a/api/README.md b/api/README.md
index 24befa61a7..eab5c4026b 100644
--- a/api/README.md
+++ b/api/README.md
@@ -19,4 +19,8 @@
# manager-api
-This is a back-end project that the dashboard depends on, implemented through golang.
+This is a backend project which the dashboard depends on, implemented by Golang.
+
+## Installation
+
+[Please refer to the doc](../README.md)
diff --git a/api/build.sh b/api/build.sh
index 086fe73c51..cd704900f6 100644
--- a/api/build.sh
+++ b/api/build.sh
@@ -1,21 +1,22 @@
#!/bin/sh
#
# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
#
+export ENV=prod
pwd=`pwd`
sed -i -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
@@ -25,6 +26,6 @@ sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
-cd /root/manager-api
+cd /go/manager-api
exec ./manager-api
diff --git a/api/conf/conf.go b/api/conf/conf.go
index f4d7b77402..cbd7f8ebed 100644
--- a/api/conf/conf.go
+++ b/api/conf/conf.go
@@ -31,7 +31,7 @@ const PROD = "prod"
const BETA = "beta"
const DEV = "dev"
const LOCAL = "local"
-const confPath = "/root/manager-api/conf.json"
+const confPath = "/go/manager-api/conf.json"
const RequestId = "requestId"
var (
@@ -45,6 +45,7 @@ func init() {
setEnvironment()
initMysql()
initApisix()
+ initAuthentication()
}
func setEnvironment() {
@@ -74,7 +75,22 @@ type mysqlConfig struct {
MaxLifeTime int
}
+type user struct {
+ Username string
+ Password string
+}
+
+type authenticationConfig struct {
+ Session struct {
+ Secret string
+ ExpireTime uint64
+ }
+}
+
+var UserList = make(map[string]user, 1)
+
var MysqlConfig mysqlConfig
+var AuthenticationConfig authenticationConfig
func initMysql() {
filePath := configurationPath()
@@ -103,3 +119,22 @@ func initApisix() {
ApiKey = apisixConf.Get("api_key").String()
}
}
+
+func initAuthentication() {
+ filePath := configurationPath()
+ if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+ panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+ } else {
+ configuration := gjson.ParseBytes(configurationContent)
+ userList := configuration.Get("authentication.user").Array()
+
+ // create user list
+ for _, item := range userList {
+ username := item.Map()["username"].String()
+ password := item.Map()["password"].String()
+ UserList[item.Map()["username"].String()] = user{Username: username, Password: password}
+ }
+ AuthenticationConfig.Session.Secret = configuration.Get("authentication.session.secret").String()
+ AuthenticationConfig.Session.ExpireTime = configuration.Get("authentication.session.expireTime").Uint()
+ }
+}
diff --git a/api/conf/conf.json b/api/conf/conf.json
index 95fe86e8c9..d160087da5 100644
--- a/api/conf/conf.json
+++ b/api/conf/conf.json
@@ -1,6 +1,6 @@
{
- "conf":{
- "mysql":{
+ "conf": {
+ "mysql": {
"address": "127.0.0.1:3306",
"user": "root",
"password": "123456",
@@ -8,12 +8,28 @@
"maxIdleConns": 25,
"maxLifeTime": 10
},
- "syslog":{
- "host": "localhost"
+ "syslog": {
+ "host": "127.0.0.1"
},
- "apisix":{
+ "apisix": {
"base_url": "http://127.0.0.1:9080/apisix/admin",
"api_key": "edd1c9f034335f136f87ad84b625c8f1"
}
+ },
+ "authentication": {
+ "session": {
+ "secret": "secret",
+ "expireTime": 3600
+ },
+ "user": [
+ {
+ "username": "admin",
+ "password": "admin"
+ },
+ {
+ "username": "user",
+ "password": "user"
+ }
+ ]
}
-}
\ No newline at end of file
+}
diff --git a/api/conf.json b/api/conf/conf_preview.json
similarity index 54%
rename from api/conf.json
rename to api/conf/conf_preview.json
index 31cbcce97d..07e304a4d8 100644
--- a/api/conf.json
+++ b/api/conf/conf_preview.json
@@ -1,6 +1,6 @@
{
"conf": {
- "mysql":{
+ "mysql": {
"address": "#mysqlAddress#",
"user": "#mysqlUser#",
"password": "#mysqlPWD#",
@@ -15,5 +15,21 @@
"base_url": "#apisixBaseUrl#",
"api_key": "#apisixApiKey#"
}
+ },
+ "authentication": {
+ "session": {
+ "secret": "secret",
+ "expireTime": 3600
+ },
+ "user": [
+ {
+ "username": "admin",
+ "password": "admin"
+ },
+ {
+ "username": "user",
+ "password": "user"
+ }
+ ]
}
}
diff --git a/api/docker-compose.yml b/api/docker-compose.yml
index 79760d7ae4..4f838116d0 100644
--- a/api/docker-compose.yml
+++ b/api/docker-compose.yml
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
version: "3"
services:
diff --git a/api/errno/error.go b/api/errno/error.go
index 2c3917dd86..93180d89ee 100644
--- a/api/errno/error.go
+++ b/api/errno/error.go
@@ -22,63 +22,78 @@ import (
)
type Message struct {
- Code string
- Msg string
+ Code string
+ Msg string
+ Status int `json:"-"`
}
var (
//AA 01 api-manager-api
- SystemSuccess = Message{"010000", "success"}
- SystemError = Message{"010001", "system error"}
- BadRequestError = Message{Code: "010002", Msg: "Request format error"}
- NotFoundError = Message{Code: "010003", Msg: "No resources found"}
- InvalidParam = Message{"010004", "Request parameter error"}
- DBWriteError = Message{"010005", "Database save failed"}
- DBReadError = Message{"010006", "Database query failed"}
- DBDeleteError = Message{"010007", "Database delete failed"}
- RecordNotExist = Message{"010009", "Record does not exist"}
-
- //BB 01 config module
- ConfEnvError = Message{"010101", "Environment variable not found: %s"}
- ConfFilePathError = Message{"010102", "Error loading configuration file: %s"}
-
- // BB 02 route module
- RouteRequestError = Message{"010201", "Route request parameters are abnormal: %s"}
- ApisixRouteCreateError = Message{"010202", "Failed to create APISIX route: %s"}
- DBRouteCreateError = Message{"010203", "Route storage failure: %s"}
- ApisixRouteUpdateError = Message{"010204", "Update APISIX routing failed: %s"}
- ApisixRouteDeleteError = Message{"010205", "Failed to remove APISIX route: %s"}
- DBRouteUpdateError = Message{"010206", "Route update failed: %s"}
- DBRouteDeleteError = Message{"010207", "Route remove failed: %s"}
-
- // 03 plugin module
- ApisixPluginListError = Message{"010301", "List APISIX plugins failed: %s"}
- ApisixPluginSchemaError = Message{"010301", "Find APISIX plugin schema failed: %s"}
-
- // 04 ssl模块
- SslParseError = Message{"010401", "Certificate resolution failed: %s"}
- ApisixSslCreateError = Message{"010402", "Create APISIX SSL failed"}
- ApisixSslUpdateError = Message{"010403", "Update APISIX SSL failed"}
- ApisixSslDeleteError = Message{"010404", "Delete APISIX SSL failed"}
+ //BB 00 system
+ SystemSuccess = Message{"010000", "success", 200}
+ SystemError = Message{"010001", "system error", 500}
+ BadRequestError = Message{"010002", "Request format error", 400}
+ NotFoundError = Message{"010003", "No resources found", 404}
+ InvalidParam = Message{"010004", "Request parameter error", 400}
+ DBWriteError = Message{"010005", "Database save failed", 500}
+ DBReadError = Message{"010006", "Database query failed", 500}
+ DBDeleteError = Message{"010007", "Database delete failed", 500}
+ RecordNotExist = Message{"010009", "Record does not exist", 404}
+ InvalidParamDetail = Message{"010010", "Invalid request parameter: %s", 400}
+ AdminApiSaveError = Message{"010011", "Data save failed", 500}
+ SchemaCheckFailed = Message{"010012", "%s", 400}
+ ForbiddenError = Message{"010013", "Request Unauthorized", 401}
+
+ //BB 01 configuration
+ ConfEnvError = Message{"010101", "Environment variable not found: %s", 500}
+ ConfFilePathError = Message{"010102", "Error loading configuration file: %s", 500}
+
+ // BB 02 route
+ RouteRequestError = Message{"010201", "Route request parameters are abnormal: %s", 400}
+ ApisixRouteCreateError = Message{"010202", "Failed to create APISIX route: %s", 500}
+ DBRouteCreateError = Message{"010203", "Route storage failure: %s", 500}
+ ApisixRouteUpdateError = Message{"010204", "Update APISIX routing failed: %s", 500}
+ ApisixRouteDeleteError = Message{"010205", "Failed to delete APISIX route: %s", 500}
+ DBRouteUpdateError = Message{"010206", "Route update failed: %s", 500}
+ DBRouteDeleteError = Message{"010207", "Route deletion failed: %s", 500}
+ DBRouteReduplicateError = Message{"010208", "Route name is reduplicate : %s", 400}
+
+ // 03 plugins
+ ApisixPluginListError = Message{"010301", "find APISIX plugin list failed: %s", 500}
+ ApisixPluginSchemaError = Message{"010301", "find APISIX plugin schema failed: %s", 500}
+
+ // 04 ssl
+ SslParseError = Message{"010401", "Certificate resolution failed: %s", 400}
+ ApisixSslCreateError = Message{"010402", "Failed to create APISIX SSL", 500}
+ ApisixSslUpdateError = Message{"010403", "Failed to update APISIX SSL", 500}
+ ApisixSslDeleteError = Message{"010404", "Failed to delete APISIX SSL", 500}
+ SslForSniNotExists = Message{"010407", "Ssl for sni not exists:%s", 400}
+ DuplicateSslCert = Message{"010408", "Duplicate ssl cert", 400}
// 06 upstream
- UpstreamRequestError = Message{"010601", "upstream request parameters are abnormal: %s"}
- UpstreamTransError = Message{"010602", "upstream parameter conversion is abnormal: %s"}
- DBUpstreamError = Message{"010603", "upstream storage failure: %s"}
- ApisixUpstreamCreateError = Message{"010604", "apisix upstream create failure: %s"}
- ApisixUpstreamUpdateError = Message{"010605", "apisix upstream update failure: %s"}
- ApisixUpstreamDeleteError = Message{"010606", "apisix upstream delete failure: %s"}
- DBUpstreamDeleteError = Message{"010607", "upstream delete failure: %s"}
-
- ApisixConsumerCreateError = Message{"010702", "Create APISIX Consumer failed"}
- ApisixConsumerUpdateError = Message{"010703", "Update APISIX Consumer failed"}
- ApisixConsumerDeleteError = Message{"010704", "Delete APISIX Consumer failed"}
- DuplicateUserName = Message{"010705", "Duplicate username"}
+ UpstreamRequestError = Message{"010601", "upstream request parameters exception: %s", 400}
+ UpstreamTransError = Message{"010602", "Abnormal upstream parameter conversion: %s", 400}
+ DBUpstreamError = Message{"010603", "upstream storage failure: %s", 500}
+ ApisixUpstreamCreateError = Message{"010604", "apisix upstream create failed: %s", 500}
+ ApisixUpstreamUpdateError = Message{"010605", "apisix upstream update failed: %s", 500}
+ ApisixUpstreamDeleteError = Message{"010606", "apisix upstream delete failed: %s", 500}
+ DBUpstreamDeleteError = Message{"010607", "upstream storage delete failed: %s", 500}
+ DBUpstreamReduplicateError = Message{"010608", "Upstream name is reduplicate : %s", 500}
+
+ // 07 consumer
+ ApisixConsumerCreateError = Message{"010702", "APISIX Consumer create failed", 500}
+ ApisixConsumerUpdateError = Message{"010703", "APISIX Consumer update failed", 500}
+ ApisixConsumerDeleteError = Message{"010704", "APISIX Consumer delete failed", 500}
+ DuplicateUserName = Message{"010705", "Duplicate consumer username", 400}
+
+ // 99 authentication
+ AuthenticationUserError = Message{"019901", "username or password error", 401}
)
type ManagerError struct {
TraceId string
Code string
+ Status int
Msg string
Data interface{}
Detail string
@@ -94,11 +109,11 @@ func (e *ManagerError) ErrorDetail() string {
}
func FromMessage(m Message, args ...interface{}) *ManagerError {
- return &ManagerError{TraceId: "", Code: m.Code, Msg: fmt.Sprintf(m.Msg, args...)}
+ return &ManagerError{TraceId: "", Code: m.Code, Status: m.Status, Msg: fmt.Sprintf(m.Msg, args...)}
}
func New(m Message, args ...interface{}) *ManagerError {
- return &ManagerError{TraceId: "", Code: m.Code, Msg: m.Msg, Detail: fmt.Sprintf("%s", args...)}
+ return &ManagerError{TraceId: "", Code: m.Code, Msg: m.Msg, Status: m.Status, Detail: fmt.Sprintf("%s", args...)}
}
func (e *ManagerError) Response() map[string]interface{} {
@@ -137,9 +152,9 @@ func Succeed() map[string]interface{} {
type HttpError struct {
Code int
- Msg string
+ Msg Message
}
func (e *HttpError) Error() string {
- return e.Msg
+ return e.Msg.Msg
}
diff --git a/api/filter/authentication.go b/api/filter/authentication.go
new file mode 100644
index 0000000000..0a2bfde1e0
--- /dev/null
+++ b/api/filter/authentication.go
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "strings"
+)
+
+func Authentication() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if c.Request.URL.Path != "/apisix/admin/user/login" && strings.HasPrefix(c.Request.URL.Path,"/apisix") {
+ tokenStr := c.GetHeader("Authorization")
+
+ // verify token
+ token, err := jwt.ParseWithClaims(tokenStr, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
+ return []byte(conf.AuthenticationConfig.Session.Secret), nil
+ })
+
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ claims, ok := token.Claims.(*jwt.StandardClaims)
+ if !ok {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ if err := token.Claims.Valid(); err != nil {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ if claims.Subject == "" {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ if _, ok := conf.UserList[claims.Subject]; !ok {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+ }
+ c.Next()
+ }
+}
diff --git a/api/filter/cors.go b/api/filter/cors.go
index f2c7ac1537..b33c62b94f 100644
--- a/api/filter/cors.go
+++ b/api/filter/cors.go
@@ -22,7 +22,7 @@ func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
- c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
+ c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
diff --git a/api/filter/logging.go b/api/filter/logging.go
index b1a40f6b3b..6318d52c00 100644
--- a/api/filter/logging.go
+++ b/api/filter/logging.go
@@ -18,6 +18,7 @@ package filter
import (
"bytes"
+ "io/ioutil"
"time"
"github.com/apisix/manager-api/errno"
@@ -33,11 +34,15 @@ func RequestLogHandler() gin.HandlerFunc {
val = c.Request.URL.Query()
} else {
val, _ = c.GetRawData()
+
+ // set RequestBody back
+ c.Request.Body = ioutil.NopCloser(bytes.NewReader(val.([]byte)))
}
c.Set("requestBody", val)
uuid, _ := c.Get("X-Request-Id")
param, _ := c.Get("requestBody")
+
switch param.(type) {
case []byte:
param = string(param.([]byte))
diff --git a/api/go.mod b/api/go.mod
index 45372ec421..0ebf63916d 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -3,13 +3,17 @@ module github.com/apisix/manager-api
go 1.13
require (
+ github.com/api7/apitest v1.4.9
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/pprof v1.3.0
+ github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.6.3
- github.com/go-sql-driver/mysql v1.5.0
+ github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/jinzhu/gorm v1.9.12
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.6.0
- github.com/stretchr/testify v1.4.0
+ github.com/steinfletcher/apitest v1.4.9 // indirect
+ github.com/stretchr/testify v1.6.1
github.com/tidwall/gjson v1.6.0
gopkg.in/resty.v1 v1.12.0
)
diff --git a/api/go.sum b/api/go.sum
index 9e3e3e32a6..b82d22e1e7 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,18 +1,34 @@
+github.com/api7/apitest v1.4.9 h1:FYTUQJ1hgeB9UvMFif1jjbfiA+XqHPEBfsjhDskytA8=
+github.com/api7/apitest v1.4.9/go.mod h1:YZruZ+jDMFL6rNgMWiuhwCTugNN0mJkLCYCHG3ICYlE=
+github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
+github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
+github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
+github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
+github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
@@ -20,41 +36,64 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
+github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
+github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/steinfletcher/apitest v1.4.9 h1:8X7G+1m+GngIo5LFfDM0CxLSG9jcJn9LLeDH/Ov144M=
+github.com/steinfletcher/apitest v1.4.9/go.mod h1:0MT98QwexQVvf5pIn3fqiC/+8Nyd7A4RShxuSjnpOcE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
@@ -67,6 +106,7 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -75,15 +115,21 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/api/main.go b/api/main.go
index f58c65b0f4..9c4918fb4b 100644
--- a/api/main.go
+++ b/api/main.go
@@ -22,40 +22,17 @@ import (
"time"
"github.com/apisix/manager-api/conf"
- "github.com/apisix/manager-api/filter"
"github.com/apisix/manager-api/log"
"github.com/apisix/manager-api/route"
- "github.com/gin-contrib/pprof"
- "github.com/gin-gonic/gin"
)
var logger = log.GetLogger()
-func setUpRouter() *gin.Engine {
- if conf.ENV != conf.LOCAL && conf.ENV != conf.BETA {
- gin.SetMode(gin.DebugMode)
- } else {
- gin.SetMode(gin.ReleaseMode)
- }
- r := gin.New()
-
- r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
- route.AppendHealthCheck(r)
- route.AppendRoute(r)
- route.AppendSsl(r)
- route.AppendPlugin(r)
- route.AppendUpstream(r)
-
- pprof.Register(r)
-
- return r
-}
-
func main() {
// init
conf.InitializeMysql()
// routes
- r := setUpRouter()
+ r := route.SetUpRouter()
addr := fmt.Sprintf(":%d", conf.ServerPort)
s := &http.Server{
Addr: addr,
@@ -63,5 +40,7 @@ func main() {
ReadTimeout: time.Duration(1000) * time.Millisecond,
WriteTimeout: time.Duration(5000) * time.Millisecond,
}
- s.ListenAndServe()
+ if err := s.ListenAndServe(); err != nil {
+ panic(err)
+ }
}
diff --git a/api/route/authentication.go b/api/route/authentication.go
new file mode 100644
index 0000000000..98348959e5
--- /dev/null
+++ b/api/route/authentication.go
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ jwt "github.com/dgrijalva/jwt-go"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "time"
+)
+
+type UserSession struct {
+ Token string `json:"token"`
+}
+
+func AppendAuthentication(r *gin.Engine) *gin.Engine {
+ r.POST("/apisix/admin/user/login", userLogin)
+ return r
+}
+
+func userLogin(c *gin.Context) {
+ username := c.PostForm("username")
+ password := c.PostForm("password")
+
+ if username == "" {
+ c.AbortWithStatusJSON(http.StatusBadRequest, errno.FromMessage(errno.InvalidParamDetail, "username is needed").Response())
+ return
+ }
+ if password == "" {
+ c.AbortWithStatusJSON(http.StatusBadRequest, errno.FromMessage(errno.InvalidParamDetail, "password is needed").Response())
+ return
+ }
+
+ user := conf.UserList[username]
+ if username != user.Username || password != user.Password {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.AuthenticationUserError).Response())
+ } else {
+ // create JWT for session
+ claims := jwt.StandardClaims{
+ Subject: username,
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(time.Second * time.Duration(conf.AuthenticationConfig.Session.ExpireTime)).Unix(),
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ signedToken, _ := token.SignedString([]byte(conf.AuthenticationConfig.Session.Secret))
+
+ // output token
+ c.AbortWithStatusJSON(http.StatusOK, errno.FromMessage(errno.SystemSuccess).ItemResponse(&UserSession {
+ Token: signedToken,
+ }))
+ }
+}
diff --git a/api/route/authentication_test.go b/api/route/authentication_test.go
new file mode 100644
index 0000000000..89c1b4b3d4
--- /dev/null
+++ b/api/route/authentication_test.go
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "bytes"
+ "net/http"
+ "strings"
+ "testing"
+)
+
+var token string
+
+func TestUserLogin(t *testing.T) {
+ // password error
+ handler.
+ Post("/apisix/admin/user/login").
+ Header("Content-Type", "application/x-www-form-urlencoded").
+ Body("username=admin&password=admin1").
+ Expect(t).
+ Status(http.StatusUnauthorized).
+ End()
+
+ // login success
+ sessionResponse := handler.
+ Post("/apisix/admin/user/login").
+ Header("Content-Type", "application/x-www-form-urlencoded").
+ Body("username=admin&password=admin").
+ Expect(t).
+ Status(http.StatusOK).
+ End().Response.Body
+
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(sessionResponse)
+ data := buf.String()
+ tokenArr := strings.Split(data, "\"token\":\"")
+ token = strings.Split(tokenArr[1], "\"}")[0]
+}
diff --git a/api/route/base.go b/api/route/base.go
new file mode 100644
index 0000000000..1b48d4a0ca
--- /dev/null
+++ b/api/route/base.go
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "github.com/gin-contrib/pprof"
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-contrib/sessions/cookie"
+ "github.com/gin-gonic/gin"
+
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/filter"
+)
+
+func SetUpRouter() *gin.Engine {
+ if conf.ENV != conf.LOCAL && conf.ENV != conf.BETA {
+ gin.SetMode(gin.DebugMode)
+ } else {
+ gin.SetMode(gin.ReleaseMode)
+ }
+ r := gin.New()
+ store := cookie.NewStore([]byte("secret"))
+ r.Use(sessions.Sessions("session", store))
+ r.Use(filter.CORS(), filter.Authentication(),filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
+
+ AppendHealthCheck(r)
+ AppendAuthentication(r)
+ AppendRoute(r)
+ AppendSsl(r)
+ AppendPlugin(r)
+ AppendUpstream(r)
+ AppendConsumer(r)
+
+ pprof.Register(r)
+
+ return r
+}
diff --git a/api/route/base_test.go b/api/route/base_test.go
new file mode 100644
index 0000000000..007af13f0c
--- /dev/null
+++ b/api/route/base_test.go
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "github.com/api7/apitest"
+ "github.com/apisix/manager-api/conf"
+)
+
+var handler *apitest.APITest
+
+var (
+ uriPrefix = "/apisix/admin"
+)
+
+func init() {
+ //init mysql connect
+ conf.InitializeMysql()
+
+ r := SetUpRouter()
+
+ handler = apitest.
+ New().
+ Handler(r)
+}
+
+
diff --git a/api/route/consumer.go b/api/route/consumer.go
index 8c442b61a8..3f0bfa1482 100644
--- a/api/route/consumer.go
+++ b/api/route/consumer.go
@@ -39,6 +39,22 @@ func AppendConsumer(r *gin.Engine) *gin.Engine {
return r
}
+func handleServiceError(c *gin.Context, requestId interface{}, err error) {
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+
+ e := err.(*errno.ManagerError)
+ status := http.StatusInternalServerError
+ if e.Status > 0 {
+ status = e.Status
+ }
+ logger.WithField(conf.RequestId, requestId).Error(e.ErrorDetail())
+ c.AbortWithStatusJSON(status, e.Response())
+}
+
func consumerList(c *gin.Context) {
requestId, _ := c.Get("X-Request-Id")
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
@@ -49,8 +65,7 @@ func consumerList(c *gin.Context) {
count, list, err := service.ConsumerList(page, size, search)
if err != nil {
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -66,8 +81,7 @@ func consumerItem(c *gin.Context) {
consumer, err := service.ConsumerItem(id)
if err != nil {
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -88,13 +102,7 @@ func consumerCreate(c *gin.Context) {
}
if err := service.ConsumerCreate(param, u4.String()); err != nil {
- if httpError, ok := err.(*errno.HttpError); ok {
- logger.WithField(conf.RequestId, requestId).Error(err)
- c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
- return
- }
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -115,13 +123,7 @@ func consumerUpdate(c *gin.Context) {
}
if err := service.ConsumerUpdate(param, id); err != nil {
- if httpError, ok := err.(*errno.HttpError); ok {
- logger.WithField(conf.RequestId, requestId).Error(err)
- c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
- return
- }
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -133,14 +135,7 @@ func consumerDelete(c *gin.Context) {
id := c.Param("id")
if err := service.ConsumerDelete(id); err != nil {
- if httpError, ok := err.(*errno.HttpError); ok {
- logger.WithField(conf.RequestId, requestId).Error(err)
- c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
- return
- }
-
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
diff --git a/api/route/consumer_test.go b/api/route/consumer_test.go
new file mode 100644
index 0000000000..b32c38fe2a
--- /dev/null
+++ b/api/route/consumer_test.go
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/apisix/manager-api/service"
+)
+
+func TestConsumer(t *testing.T) {
+ // create ok
+ handler.
+ Post(uriPrefix + "/consumers").
+ Header("Authorization", token).
+ JSON(`{
+ "username": "e2e_test_consumer1",
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ },
+ "basic-auth": {
+ "username": "foo",
+ "password": "bar"
+ }
+ },
+ "desc": "test description"
+ }`).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+
+ c1, _ := service.GetConsumerByUserName("e2e_test_consumer1")
+
+ //update ok
+ handler.
+ Put(uriPrefix + "/consumers/" + c1.ID.String()).
+ JSON(`{
+ "username": "e2e_test_consumer1",
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ },
+ "basic-auth": {
+ "username": "foo",
+ "password": "bar"
+ }
+ },
+ "desc": "test desc"
+ }`).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+
+ // duplicate username
+ handler.
+ Post(uriPrefix + "/consumers").
+ JSON(`{
+ "username": "e2e_test_consumer1",
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ },
+ "basic-auth": {
+ "username": "foo",
+ "password": "bar"
+ }
+ },
+ "desc": "test description"
+ }`).
+ Expect(t).
+ Status(http.StatusBadRequest).
+ End()
+}
diff --git a/api/route/route.go b/api/route/route.go
index 82d5106a4f..c86b437fbb 100644
--- a/api/route/route.go
+++ b/api/route/route.go
@@ -34,9 +34,44 @@ func AppendRoute(r *gin.Engine) *gin.Engine {
r.GET("/apisix/admin/routes", listRoute)
r.PUT("/apisix/admin/routes/:rid", updateRoute)
r.DELETE("/apisix/admin/routes/:rid", deleteRoute)
+ r.GET("/apisix/admin/notexist/routes", isRouteExist)
return r
}
+func isRouteExist(c *gin.Context) {
+ if name, exist := c.GetQuery("name"); exist {
+ db := conf.DB()
+ db = db.Table("routes")
+ exclude, exist := c.GetQuery("exclude")
+ if exist {
+ db = db.Where("name=? and id<>?", name, exclude)
+ } else {
+ db = db.Where("name=?", name)
+ }
+ var count int
+ err := db.Count(&count).Error
+ if err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ } else {
+ if count == 0 {
+ c.Data(http.StatusOK, service.ContentType, errno.Success())
+ return
+ } else {
+ e := errno.FromMessage(errno.DBRouteReduplicateError, name)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+ }
+ } else {
+ e := errno.FromMessage(errno.RouteRequestError, "name is needed")
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+}
+
func listRoute(c *gin.Context) {
db := conf.DB()
size, _ := strconv.Atoi(c.Query("size"))
@@ -44,6 +79,7 @@ func listRoute(c *gin.Context) {
if size == 0 {
size = 10
}
+ db = db.Table("routes")
isSearch := true
if name, exist := c.GetQuery("name"); exist {
db = db.Where("name like ? ", "%"+name+"%")
@@ -68,18 +104,14 @@ func listRoute(c *gin.Context) {
// search
if isSearch {
if search, exist := c.GetQuery("search"); exist {
- db = db.Where("name like ? ", "%"+search+"%").
- Or("description like ? ", "%"+search+"%").
- Or("hosts like ? ", "%"+search+"%").
- Or("uris like ? ", "%"+search+"%").
- Or("upstream_nodes like ? ", "%"+search+"%")
+ s := "%" + search + "%"
+ db = db.Where("name like ? or description like ? or hosts like ? or uris like ? or upstream_nodes like ? ", s, s, s, s, s)
}
}
- // todo params check
// mysql
routeList := []service.Route{}
var count int
- if err := db.Order("priority, update_time desc").Table("routes").Offset((page - 1) * size).Limit(size).Find(&routeList).Count(&count).Error; err != nil {
+ if err := db.Order("priority, update_time desc").Table("routes").Offset((page - 1) * size).Limit(size).Find(&routeList).Error; err != nil {
e := errno.FromMessage(errno.RouteRequestError, err.Error())
logger.Error(e.Msg)
c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
@@ -91,6 +123,12 @@ func listRoute(c *gin.Context) {
response.Parse(&r)
responseList = append(responseList, *response)
}
+ if err := db.Count(&count).Error; err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
result := &service.ListResponse{Count: count, Data: responseList}
resp, _ := json.Marshal(result)
c.Data(http.StatusOK, service.ContentType, resp)
@@ -99,30 +137,45 @@ func listRoute(c *gin.Context) {
func deleteRoute(c *gin.Context) {
rid := c.Param("rid")
- // todo params check
- // delete from apisix
- request := &service.ApisixRouteRequest{}
- if _, err := request.Delete(rid); err != nil {
- e := errno.FromMessage(errno.ApisixRouteDeleteError, err.Error())
+ db := conf.DB()
+ tx := db.Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+ // delete from mysql
+ rd := &service.Route{}
+ rd.ID = uuid.FromStringOrNil(rid)
+ if err := conf.DB().Delete(rd).Error; err != nil {
+ tx.Rollback()
+ e := errno.FromMessage(errno.DBRouteDeleteError, err.Error())
logger.Error(e.Msg)
c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
return
} else {
- // delete from mysql
- rd := &service.Route{}
- rd.ID = uuid.FromStringOrNil(rid)
- if err := conf.DB().Delete(rd).Error; err != nil {
- e := errno.FromMessage(errno.DBRouteDeleteError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
- return
+ request := &service.ApisixRouteRequest{}
+ if _, err := request.Delete(rid); err != nil {
+ tx.Rollback()
+ if httpError, ok := err.(*errno.HttpError); ok {
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ } else {
+ e := errno.FromMessage(errno.ApisixRouteDeleteError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
}
}
+ if err := tx.Commit().Error; err != nil {
+ e := errno.FromMessage(errno.ApisixRouteDeleteError, err.Error())
+ logger.Error(e.Msg)
+ }
c.Data(http.StatusOK, service.ContentType, errno.Success())
}
func updateRoute(c *gin.Context) {
rid := c.Param("rid")
- // todo params check
param, exist := c.Get("requestBody")
if !exist || len(param.([]byte)) < 1 {
e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
@@ -138,27 +191,52 @@ func updateRoute(c *gin.Context) {
return
}
logger.Info(routeRequest.Plugins)
-
+ db := conf.DB()
arr := service.ToApisixRequest(routeRequest)
- logger.Info(arr)
- if resp, err := arr.Update(rid); err != nil {
- e := errno.FromMessage(errno.ApisixRouteUpdateError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ var resp *service.ApisixRouteResponse
+ if rd, err := service.ToRoute(routeRequest, arr, uuid.FromStringOrNil(rid), nil); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
return
} else {
- // update mysql
- if rd, err := service.ToRoute(routeRequest, arr, uuid.FromStringOrNil(rid), resp); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+ tx := db.Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+ logger.Info(rd)
+ if err := tx.Model(&service.Route{}).Update(rd).Error; err != nil {
+ // rollback
+ tx.Rollback()
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
return
} else {
- if err := conf.DB().Model(&service.Route{}).Update(rd).Error; err != nil {
+ if resp, err = arr.Update(rid); err != nil {
+ tx.Rollback()
+ if httpError, ok := err.(*errno.HttpError); ok {
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ } else {
+ e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+ }
+ }
+ if err := tx.Commit().Error; err == nil {
+ // update content_admin_api
+ if rd, err := service.ToRoute(routeRequest, arr, uuid.FromStringOrNil(rid), resp); err != nil {
e := errno.FromMessage(errno.DBRouteUpdateError, err.Error())
logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
- return
+ } else {
+ if err := conf.DB().Model(&service.Route{}).Update(rd).Error; err != nil {
+ e := errno.FromMessage(errno.DBRouteUpdateError, err.Error())
+ logger.Error(e.Msg)
+ }
}
- logger.Info(rd)
}
}
c.Data(http.StatusOK, service.ContentType, errno.Success())
@@ -166,7 +244,20 @@ func updateRoute(c *gin.Context) {
func findRoute(c *gin.Context) {
rid := c.Param("rid")
- // todo params check
+ var count int
+ if err := conf.DB().Table("routes").Where("id=?", rid).Count(&count).Error; err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ } else {
+ if count < 1 {
+ e := errno.FromMessage(errno.RouteRequestError, " route ID: "+rid+" not exist")
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(e.Status, e.Response())
+ return
+ }
+ }
// find from apisix
request := &service.ApisixRouteRequest{}
if response, err := request.FindById(rid); err != nil {
@@ -191,6 +282,12 @@ func findRoute(c *gin.Context) {
return
}
result.Name = route.Name
+ var script map[string]interface{}
+ if err = json.Unmarshal([]byte(route.Script), &script); err != nil {
+ script = map[string]interface{}{}
+ }
+ result.Script = script
+
resp, _ := json.Marshal(result)
c.Data(http.StatusOK, service.ContentType, resp)
}
@@ -200,7 +297,6 @@ func findRoute(c *gin.Context) {
func createRoute(c *gin.Context) {
u4 := uuid.NewV4()
rid := u4.String()
- // todo params check
param, exist := c.Get("requestBody")
if !exist || len(param.([]byte)) < 1 {
e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
@@ -216,26 +312,51 @@ func createRoute(c *gin.Context) {
return
}
logger.Info(routeRequest.Plugins)
-
+ db := conf.DB()
arr := service.ToApisixRequest(routeRequest)
- logger.Info(arr)
- if resp, err := arr.Create(rid); err != nil {
- e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ var resp *service.ApisixRouteResponse
+ if rd, err := service.ToRoute(routeRequest, arr, u4, nil); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
return
} else {
- // update mysql
- if rd, err := service.ToRoute(routeRequest, arr, u4, resp); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+ tx := db.Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+ logger.Info(rd)
+ if err := tx.Create(rd).Error; err != nil {
+ // rollback
+ tx.Rollback()
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
return
} else {
- logger.Info(rd)
- if err := conf.DB().Create(rd).Error; err != nil {
- e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ if resp, err = arr.Create(rid); err != nil {
+ tx.Rollback()
+ if httpError, ok := err.(*errno.HttpError); ok {
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ } else {
+ e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+ }
+ }
+ if err := tx.Commit().Error; err == nil {
+ // update content_admin_api
+ if rd, err := service.ToRoute(routeRequest, arr, u4, resp); err != nil {
+ e := errno.FromMessage(errno.DBRouteUpdateError, err.Error())
logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
- return
+ } else {
+ if err := conf.DB().Model(&service.Route{}).Update(rd).Error; err != nil {
+ e := errno.FromMessage(errno.DBRouteUpdateError, err.Error())
+ logger.Error(e.Msg)
+ }
}
}
}
diff --git a/api/route/ssl.go b/api/route/ssl.go
index 7b62b49588..d962be8d79 100644
--- a/api/route/ssl.go
+++ b/api/route/ssl.go
@@ -30,6 +30,7 @@ import (
func AppendSsl(r *gin.Engine) *gin.Engine {
r.POST("/apisix/admin/check_ssl_cert", sslCheck)
+ r.POST("/apisix/admin/check_ssl_exists", hostCheck)
r.GET("/apisix/admin/ssls", sslList)
r.POST("/apisix/admin/ssls", sslCreate)
@@ -48,14 +49,14 @@ func sslList(c *gin.Context) {
status, _ := strconv.Atoi(c.DefaultQuery("status", "-1"))
expireStart, _ := strconv.Atoi(c.DefaultQuery("expire_start", "-1"))
expireEnd, _ := strconv.Atoi(c.DefaultQuery("expire_end", "-1"))
+ sortType := c.DefaultQuery("sort_type", "desc")
sni := c.DefaultQuery("sni", "")
- count, list, err := service.SslList(page, size, status, expireStart, expireEnd, sni)
+ count, list, err := service.SslList(page, size, status, expireStart, expireEnd, sni, sortType)
if err != nil {
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -71,8 +72,7 @@ func sslItem(c *gin.Context) {
ssl, err := service.SslItem(id)
if err != nil {
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -92,8 +92,7 @@ func sslCheck(c *gin.Context) {
ssl, err := service.SslCheck(param)
if err != nil {
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -116,13 +115,7 @@ func sslCreate(c *gin.Context) {
}
if err := service.SslCreate(param, u4.String()); err != nil {
- if httpError, ok := err.(*errno.HttpError); ok {
- logger.WithField(conf.RequestId, requestId).Error(err)
- c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
- return
- }
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -132,7 +125,6 @@ func sslCreate(c *gin.Context) {
func sslUpdate(c *gin.Context) {
requestId, _ := c.Get("X-Request-Id")
param, exist := c.Get("requestBody")
-
id := c.Param("id")
if !exist || len(param.([]byte)) < 1 {
@@ -143,13 +135,7 @@ func sslUpdate(c *gin.Context) {
}
if err := service.SslUpdate(param, id); err != nil {
- if httpError, ok := err.(*errno.HttpError); ok {
- logger.WithField(conf.RequestId, requestId).Error(err)
- c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
- return
- }
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -170,14 +156,7 @@ func sslPatch(c *gin.Context) {
}
if err := service.SslPatch(param, id); err != nil {
- if httpError, ok := err.(*errno.HttpError); ok {
- logger.WithField(conf.RequestId, requestId).Error(err)
- c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
- return
- }
-
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
@@ -189,16 +168,30 @@ func sslDelete(c *gin.Context) {
id := c.Param("id")
if err := service.SslDelete(id); err != nil {
- if httpError, ok := err.(*errno.HttpError); ok {
- logger.WithField(conf.RequestId, requestId).Error(err)
- c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
- return
- }
-
- logger.WithField(conf.RequestId, requestId).Error(err.(*errno.ManagerError).ErrorDetail())
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.(*errno.ManagerError).Response())
+ handleServiceError(c, requestId, err)
return
}
c.JSON(http.StatusOK, errno.Succeed())
}
+
+func hostCheck(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
+ param, exist := c.Get("requestBody")
+
+ if !exist || len(param.([]byte)) < 1 {
+ err := errno.New(errno.InvalidParam)
+ logger.WithField(conf.RequestId, requestId).Error(err.ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusBadRequest, err.Response())
+ return
+ }
+
+ err := service.CheckSniExists(param)
+ if err != nil {
+ handleServiceError(c, requestId, err)
+ return
+ }
+
+ c.JSON(http.StatusOK, errno.Succeed())
+
+}
diff --git a/api/route/ssl_test.go b/api/route/ssl_test.go
new file mode 100644
index 0000000000..3952b6bd55
--- /dev/null
+++ b/api/route/ssl_test.go
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "net/http"
+ "testing"
+)
+
+func TestSslCreate(t *testing.T) {
+ // ok
+ handler.
+ Post(uriPrefix + "/ssls").
+ JSON(`{
+ "key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDGO0J9xrOcmvgh\npkqHIYHCw35FTfIT5uXOSzdF49M2ZAKBQwFG0ovYT8bc0glNLB+hpDhJPL531qSP\nl1ZOe0W1ofP1u0T5Zzc9Rub/kn7RMPq0BsSC6J3rF+rQEwh1PM8qUuD8DxZ7jaOL\niMNL6SyuZIPsS1kPPBtsioukdo666tbjNMixhQbI9Wpg55abdXRFh3i7Zu/9siF1\njCGcsskjOaUOY4sYQ3i5WU/HIIRhA82XuIL+Sxd32P8bKi2UT1sqFXRjAVR7KRWo\nIVvkmSLoZb9ucV6MsccDrRYBf6rLbI1tFj9l2rY6GTFlT+6z7K/ZI60DGi/hsBfl\nDeEQ5WuxAgMBAAECggEAVHQQyucpxHGdfzCKlfGnh+Oj20Du/p2jkHUpEkSSypxn\nGM0EMTkoTTsHvTJath8zRrlhJYqUlxfCOk6+fWc1dsGN30Yuh5b6yMd5SK8QCm20\nkZhEhoU2Kl+hMY66TsBefmia46hF6tOYNq1IjwHDgHTgY35ibgQsptyLy8Ca5HTC\nrnoocP2AcKtM+qwOMGiNHpeh+/zfB91C9AszvS8H2ao5nq4u0/JavPO4A4WmVYol\n7Qv9ACY/8uaKC79syahutbkMjwGsQgYsq9G0QpcLSCuOb4vBbOb130mptSM9NzKg\nTjSxF2D8ob//roZMc1ueTpqAY6WedKV3y3BIBDKuAQKBgQDgGyEsxwR9QtA5EH/h\nJ4GiTQn0aep8G2LSlAtHGndL3sxaGGLt2pk3lNIeRAbOS3APmYskBN418JIF/Ren\nE0CYSrTaxpTs9UXXkgKNJ63Z6r+btswTAVVXG5Zoi/5JRSHRquEVmKccM4zg3v6R\ny/nVhwXigUaRuLx+wCtoaGsaUQKBgQDicXFZ0TvN8tohqc8dbmOu2A25+ifFKHUA\nn3yxZIJtbTC9bJeuwtkqIFol1DXHLqYvdD5jQT3c4z6HekcmI9sEy1YzO4a3WUTI\nP//ogjDLXj402k+WCx1Us2HASxwU5cRvOpMhfnppYPSDXqBoH196UCDmOQuS1+Q8\njyPsNQmDYQKBgQDcm5hCvf87V4QmSIm6GOvR20iLY6BCX6seZEHd0r3Q4BgGMK9i\nOahOQJ++z3Rrq3M6yAligbBFJPZ6ErUv8RHLWO9D1exQfvorxT3huke3lxDbtkya\nANwDjdK4Q+ckNXufLDm6yrTmXBC4ZIvw9fyQKASw/lV7qYFUvNN+Shv0oQKBgQC+\nraw3Z7smV0NbaXRgYh5KkuAsJPvsR38OwT3s2qgBoRqTx6eKn8Tidk+y3xlR2nRS\nLV6DkeKX6Ds1NcBH25WIWfkCNzPfnKoQveOuVELmXTugody2ijFuq4a6uASzjC93\nQim24JwPtHbxUHNeelyZ0HODqbGXO3iTji0/sAGMwQKBgQC8yDwapXgrCWK34qpN\nSdO9uA4VstI3Ovb+o3Evfp1CvJnfk56ypO2DaqbuvMJsInuWRFU40UWp7Vxyl/hP\nXvGgEI3dbBy9KWFjAKfI2Wv3i+zvJ1mAHM3u1jcX3zxOxSAN4LJVBudgkGpop1ps\nW5tWveXiXwxCUE/r9ax4mfJvXQ==\n-----END PRIVATE KEY-----",
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIEVzCCAr+gAwIBAgIQITiNM7xmudhg3pK85KDwLDANBgkqhkiG9w0BAQsFADB/\nMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKjAoBgNVBAsMIWp1bnh1\nY2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTExMC8GA1UEAwwobWtjZXJ0IGp1\nbnh1Y2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTAeFw0xOTA2MDEwMDAwMDBa\nFw0zMDA3MDgwNzQ4MDJaMFUxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj\nZXJ0aWZpY2F0ZTEqMCgGA1UECwwhanVueHVjaGVuQGp1bnh1ZGVBaXIgKGp1bnh1\nIGNoZW4pMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjtCfcaznJr4\nIaZKhyGBwsN+RU3yE+blzks3RePTNmQCgUMBRtKL2E/G3NIJTSwfoaQ4STy+d9ak\nj5dWTntFtaHz9btE+Wc3PUbm/5J+0TD6tAbEguid6xfq0BMIdTzPKlLg/A8We42j\ni4jDS+ksrmSD7EtZDzwbbIqLpHaOuurW4zTIsYUGyPVqYOeWm3V0RYd4u2bv/bIh\ndYwhnLLJIzmlDmOLGEN4uVlPxyCEYQPNl7iC/ksXd9j/GyotlE9bKhV0YwFUeykV\nqCFb5Jki6GW/bnFejLHHA60WAX+qy2yNbRY/Zdq2OhkxZU/us+yv2SOtAxov4bAX\n5Q3hEOVrsQIDAQABo3kwdzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB\nBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRU+EbJj+Hp62gCrNvb3yQk\nYnPHXDAhBgNVHREEGjAYgglyb3V0ZS5jb22CCyoucm91dGUuY29tMA0GCSqGSIb3\nDQEBCwUAA4IBgQAvKN2GEorAlx5sfUU2uiL49iFmQSMDLZQminQl1RIHTI/h+jz8\nNluZSdxDFmNq8am6B2ofD3VLl6StC/G+G6YuekPz+QrUNK4UB+8ftRmY4YRFGTQ6\nRnFli1wOq2ES9vPjKlIj77cznr8uwVHPHq8JxGbn/rx3oVDVPndXFCkJJ1DDjRT+\n22atHNzHt5bc9ut8Fq5NW61P+nnMMFShKJaPBkmm9Pf2pEOd8Y7OU8Iy1Kj65fsE\nUshGF5+RWoxdv6/9f6/uOQhmq3MEKqneUC3pjVZ8TiBlRvADxxR5krvujQswms0D\nFGpRMtGpPGMWTuptSIMwNcar/luVig7wGIBeV5ZaOlSOx3911le9mlS7+2lLqf5H\n5dsMkP30Sjv/jfrIL+SE1qeK3kjL0iIwA/PPARvhctExs9y2llT9+drbJofZUi+I\nZdYfAfyJT4htbcl7jHN8oY7vzwgTyxCcBxkbqKfBqabneutj0jfX39zP0G696tiZ\ndQFXCS4wkvw0CG0=\n-----END CERTIFICATE-----"
+ }`).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+
+ // schema fail
+ handler.
+ Post(uriPrefix + "/ssls").
+ JSON(`{
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIEVzCCAr+gAwIBAgIQITiNM7xmudhg3pK85KDwLDANBgkqhkiG9w0BAQsFADB/\nMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKjAoBgNVBAsMIWp1bnh1\nY2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTExMC8GA1UEAwwobWtjZXJ0IGp1\nbnh1Y2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTAeFw0xOTA2MDEwMDAwMDBa\nFw0zMDA3MDgwNzQ4MDJaMFUxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj\nZXJ0aWZpY2F0ZTEqMCgGA1UECwwhanVueHVjaGVuQGp1bnh1ZGVBaXIgKGp1bnh1\nIGNoZW4pMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjtCfcaznJr4\nIaZKhyGBwsN+RU3yE+blzks3RePTNmQCgUMBRtKL2E/G3NIJTSwfoaQ4STy+d9ak\nj5dWTntFtaHz9btE+Wc3PUbm/5J+0TD6tAbEguid6xfq0BMIdTzPKlLg/A8We42j\ni4jDS+ksrmSD7EtZDzwbbIqLpHaOuurW4zTIsYUGyPVqYOeWm3V0RYd4u2bv/bIh\ndYwhnLLJIzmlDmOLGEN4uVlPxyCEYQPNl7iC/ksXd9j/GyotlE9bKhV0YwFUeykV\nqCFb5Jki6GW/bnFejLHHA60WAX+qy2yNbRY/Zdq2OhkxZU/us+yv2SOtAxov4bAX\n5Q3hEOVrsQIDAQABo3kwdzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB\nBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRU+EbJj+Hp62gCrNvb3yQk\nYnPHXDAhBgNVHREEGjAYgglyb3V0ZS5jb22CCyoucm91dGUuY29tMA0GCSqGSIb3\nDQEBCwUAA4IBgQAvKN2GEorAlx5sfUU2uiL49iFmQSMDLZQminQl1RIHTI/h+jz8\nNluZSdxDFmNq8am6B2ofD3VLl6StC/G+G6YuekPz+QrUNK4UB+8ftRmY4YRFGTQ6\nRnFli1wOq2ES9vPjKlIj77cznr8uwVHPHq8JxGbn/rx3oVDVPndXFCkJJ1DDjRT+\n22atHNzHt5bc9ut8Fq5NW61P+nnMMFShKJaPBkmm9Pf2pEOd8Y7OU8Iy1Kj65fsE\nUshGF5+RWoxdv6/9f6/uOQhmq3MEKqneUC3pjVZ8TiBlRvADxxR5krvujQswms0D\nFGpRMtGpPGMWTuptSIMwNcar/luVig7wGIBeV5ZaOlSOx3911le9mlS7+2lLqf5H\n5dsMkP30Sjv/jfrIL+SE1qeK3kjL0iIwA/PPARvhctExs9y2llT9+drbJofZUi+I\nZdYfAfyJT4htbcl7jHN8oY7vzwgTyxCcBxkbqKfBqabneutj0jfX39zP0G696tiZ\ndQFXCS4wkvw0CG0=\n-----END CERTIFICATE-----"
+ }`).
+ Expect(t).
+ Status(http.StatusBadRequest).
+ End()
+
+ //schema fail 2
+ handler.
+ Post(uriPrefix + "/ssls").
+ JSON(`{
+ "key": "",
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIEVzCCAr+gAwIBAgIQITiNM7xmudhg3pK85KDwLDANBgkqhkiG9w0BAQsFADB/\nMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKjAoBgNVBAsMIWp1bnh1\nY2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTExMC8GA1UEAwwobWtjZXJ0IGp1\nbnh1Y2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTAeFw0xOTA2MDEwMDAwMDBa\nFw0zMDA3MDgwNzQ4MDJaMFUxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj\nZXJ0aWZpY2F0ZTEqMCgGA1UECwwhanVueHVjaGVuQGp1bnh1ZGVBaXIgKGp1bnh1\nIGNoZW4pMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjtCfcaznJr4\nIaZKhyGBwsN+RU3yE+blzks3RePTNmQCgUMBRtKL2E/G3NIJTSwfoaQ4STy+d9ak\nj5dWTntFtaHz9btE+Wc3PUbm/5J+0TD6tAbEguid6xfq0BMIdTzPKlLg/A8We42j\ni4jDS+ksrmSD7EtZDzwbbIqLpHaOuurW4zTIsYUGyPVqYOeWm3V0RYd4u2bv/bIh\ndYwhnLLJIzmlDmOLGEN4uVlPxyCEYQPNl7iC/ksXd9j/GyotlE9bKhV0YwFUeykV\nqCFb5Jki6GW/bnFejLHHA60WAX+qy2yNbRY/Zdq2OhkxZU/us+yv2SOtAxov4bAX\n5Q3hEOVrsQIDAQABo3kwdzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB\nBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRU+EbJj+Hp62gCrNvb3yQk\nYnPHXDAhBgNVHREEGjAYgglyb3V0ZS5jb22CCyoucm91dGUuY29tMA0GCSqGSIb3\nDQEBCwUAA4IBgQAvKN2GEorAlx5sfUU2uiL49iFmQSMDLZQminQl1RIHTI/h+jz8\nNluZSdxDFmNq8am6B2ofD3VLl6StC/G+G6YuekPz+QrUNK4UB+8ftRmY4YRFGTQ6\nRnFli1wOq2ES9vPjKlIj77cznr8uwVHPHq8JxGbn/rx3oVDVPndXFCkJJ1DDjRT+\n22atHNzHt5bc9ut8Fq5NW61P+nnMMFShKJaPBkmm9Pf2pEOd8Y7OU8Iy1Kj65fsE\nUshGF5+RWoxdv6/9f6/uOQhmq3MEKqneUC3pjVZ8TiBlRvADxxR5krvujQswms0D\nFGpRMtGpPGMWTuptSIMwNcar/luVig7wGIBeV5ZaOlSOx3911le9mlS7+2lLqf5H\n5dsMkP30Sjv/jfrIL+SE1qeK3kjL0iIwA/PPARvhctExs9y2llT9+drbJofZUi+I\nZdYfAfyJT4htbcl7jHN8oY7vzwgTyxCcBxkbqKfBqabneutj0jfX39zP0G696tiZ\ndQFXCS4wkvw0CG0=\n-----END CERTIFICATE-----"
+ }`).
+ Expect(t).
+ Status(http.StatusBadRequest).
+ End()
+
+}
diff --git a/api/route/upstream.go b/api/route/upstream.go
index 263b8bebdb..0bff0cbc92 100644
--- a/api/route/upstream.go
+++ b/api/route/upstream.go
@@ -1,3 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package route
import (
@@ -17,11 +33,69 @@ func AppendUpstream(r *gin.Engine) *gin.Engine {
r.POST("/apisix/admin/upstreams", createUpstream)
r.GET("/apisix/admin/upstreams/:uid", findUpstream)
r.GET("/apisix/admin/upstreams", listUpstream)
+ r.GET("/apisix/admin/names/upstreams", listUpstreamName)
r.PUT("/apisix/admin/upstreams/:uid", updateUpstream)
r.DELETE("/apisix/admin/upstreams/:uid", deleteUpstream)
+ r.GET("/apisix/admin/notexist/upstreams", isUpstreamExist)
return r
}
+func isUpstreamExist(c *gin.Context) {
+ if name, exist := c.GetQuery("name"); exist {
+ db := conf.DB()
+ db = db.Table("upstreams")
+ exclude, exist := c.GetQuery("exclude")
+ if exist {
+ db = db.Where("name=? and id<>?", name, exclude)
+ } else {
+ db = db.Where("name=?", name)
+ }
+ var count int
+ if err := db.Count(&count).Error; err != nil {
+ e := errno.FromMessage(errno.UpstreamRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ } else {
+ if count == 0 {
+ c.Data(http.StatusOK, service.ContentType, errno.Success())
+ return
+ } else {
+ e := errno.FromMessage(errno.DBUpstreamReduplicateError, name)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+ }
+ } else {
+ e := errno.FromMessage(errno.UpstreamRequestError, "name is needed")
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+}
+
+func listUpstreamName(c *gin.Context) {
+ db := conf.DB()
+ upstreamList := []service.UpstreamDao{}
+ var count int
+ if err := db.Order("name").Table("upstreams").Find(&upstreamList).Count(&count).Error; err != nil {
+ e := errno.FromMessage(errno.UpstreamRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ } else {
+ responseList := make([]*service.UpstreamNameResponse, 0)
+ for _, r := range upstreamList {
+ response, err := r.Parse2NameResponse()
+ if err == nil {
+ responseList = append(responseList, response)
+ }
+ }
+ result := &service.ListResponse{Count: count, Data: responseList}
+ resp, _ := json.Marshal(result)
+ c.Data(http.StatusOK, service.ContentType, resp)
+ }
+}
+
func createUpstream(c *gin.Context) {
u4 := uuid.NewV4()
uid := u4.String()
@@ -43,31 +117,57 @@ func createUpstream(c *gin.Context) {
}
ur.Id = uid
fmt.Println(ur)
- if aur, err := ur.Parse2Apisix(); err != nil {
- e := errno.FromMessage(errno.UpstreamTransError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ // mysql
+ if ud, err := service.Trans2UpstreamDao(nil, ur); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
return
} else {
- // apisix
- if resp, err := aur.Create(); err != nil {
- e := errno.FromMessage(errno.ApisixUpstreamCreateError, err.Error())
+ // transaction
+ db := conf.DB()
+ tx := db.Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+ if err := tx.Create(ud).Error; err != nil {
+ tx.Rollback()
+ e := errno.FromMessage(errno.DBUpstreamError, err.Error())
logger.Error(e.Msg)
c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
return
} else {
- // mysql
- fmt.Println(resp.UNode.UValue.Id)
- fmt.Println(resp.UNode.UValue.Upstream.Nodes)
- if ud, err := service.Trans2UpstreamDao(resp, ur); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+ // apisix
+ if aur, err := ur.Parse2Apisix(); err != nil {
+ tx.Rollback()
+ e := errno.FromMessage(errno.UpstreamTransError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
return
} else {
- if err := conf.DB().Create(ud).Error; err != nil {
- e := errno.FromMessage(errno.DBUpstreamError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
- return
+ if resp, err := aur.Create(); err != nil {
+ tx.Rollback()
+ if httpError, ok := err.(*errno.HttpError); ok {
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ } else {
+ e := errno.FromMessage(errno.ApisixUpstreamCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+ } else {
+ if err := tx.Commit().Error; err == nil {
+ if ud, err := service.Trans2UpstreamDao(resp, ur); err != nil {
+ e := errno.FromMessage(errno.DBUpstreamError, err.Error())
+ logger.Error(e.Msg)
+ } else {
+ if err := conf.DB().Model(&service.UpstreamDao{}).Update(ud).Error; err != nil {
+ e := errno.FromMessage(errno.DBUpstreamError, err.Error())
+ logger.Error(e.Msg)
+ }
+ }
+ }
}
}
}
@@ -77,6 +177,22 @@ func createUpstream(c *gin.Context) {
func findUpstream(c *gin.Context) {
uid := c.Param("uid")
+ upstream := &service.UpstreamDao{}
+ var count int
+ if err := conf.DB().Table("upstreams").Where("id=?", uid).Count(&count).Error; err != nil {
+ e := errno.FromMessage(errno.UpstreamRequestError, err.Error()+" upstream ID: "+uid)
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ } else {
+ if count < 1 {
+ e := errno.FromMessage(errno.UpstreamRequestError, " upstream ID: "+uid+" not exist")
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(e.Status, e.Response())
+ return
+ }
+ }
+ conf.DB().Table("upstreams").Where("id=?", uid).First(&upstream)
// find from apisix
aur := &service.ApisixUpstreamRequest{Id: uid}
if resp, err := aur.FindById(); err != nil {
@@ -104,9 +220,14 @@ func listUpstream(c *gin.Context) {
size = 10
}
db := conf.DB()
+ db = db.Table("upstreams")
+ if search, exist := c.GetQuery("search"); exist {
+ s := "%" + search + "%"
+ db = db.Where("name like ? or description like ? or nodes like ? ", s, s, s)
+ }
upstreamList := []service.UpstreamDao{}
var count int
- if err := db.Order("update_time desc").Table("upstreams").Offset((page - 1) * size).Limit(size).Find(&upstreamList).Count(&count).Error; err != nil {
+ if err := db.Order("update_time desc").Offset((page - 1) * size).Limit(size).Find(&upstreamList).Error; err != nil {
e := errno.FromMessage(errno.RouteRequestError, err.Error())
logger.Error(e.Msg)
c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
@@ -119,6 +240,12 @@ func listUpstream(c *gin.Context) {
responseList = append(responseList, response)
}
}
+ if err := db.Count(&count).Error; err != nil {
+ e := errno.FromMessage(errno.UpstreamRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
result := &service.ListResponse{Count: count, Data: responseList}
resp, _ := json.Marshal(result)
c.Data(http.StatusOK, service.ContentType, resp)
@@ -126,7 +253,6 @@ func listUpstream(c *gin.Context) {
}
func updateUpstream(c *gin.Context) {
uid := c.Param("uid")
- // todo 参数校验
param, exist := c.Get("requestBody")
if !exist || len(param.([]byte)) < 1 {
e := errno.FromMessage(errno.RouteRequestError, "upstream update with no post data")
@@ -143,30 +269,57 @@ func updateUpstream(c *gin.Context) {
return
}
ur.Id = uid
- fmt.Println(ur)
- if aur, err := ur.Parse2Apisix(); err != nil {
- e := errno.FromMessage(errno.UpstreamTransError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ // mysql
+ if ud, err := service.Trans2UpstreamDao(nil, ur); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
return
} else {
- // apisix
- if resp, err := aur.Update(); err != nil {
- e := errno.FromMessage(errno.ApisixUpstreamUpdateError, err.Error())
+ // transaction
+ db := conf.DB()
+ tx := db.Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+ if err := tx.Model(&service.UpstreamDao{}).Update(ud).Error; err != nil {
+ tx.Rollback()
+ e := errno.FromMessage(errno.DBUpstreamError, err.Error())
logger.Error(e.Msg)
c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
return
} else {
- // mysql
- if ud, err := service.Trans2UpstreamDao(resp, ur); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+ // apisix
+ if aur, err := ur.Parse2Apisix(); err != nil {
+ tx.Rollback()
+ e := errno.FromMessage(errno.UpstreamTransError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
return
} else {
- if err := conf.DB().Model(&service.UpstreamDao{}).Update(ud).Error; err != nil {
- e := errno.FromMessage(errno.DBUpstreamError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
- return
+ if resp, err := aur.Update(); err != nil {
+ tx.Rollback()
+ if httpError, ok := err.(*errno.HttpError); ok {
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ } else {
+ e := errno.FromMessage(errno.ApisixUpstreamUpdateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+ } else {
+ if err := tx.Commit().Error; err == nil {
+ if ud, err := service.Trans2UpstreamDao(resp, ur); err != nil {
+ e := errno.FromMessage(errno.DBUpstreamError, err.Error())
+ logger.Error(e.Msg)
+ } else {
+ if err := conf.DB().Model(&service.UpstreamDao{}).Update(ud).Error; err != nil {
+ e := errno.FromMessage(errno.DBUpstreamError, err.Error())
+ logger.Error(e.Msg)
+ }
+ }
+ }
}
}
}
@@ -184,23 +337,41 @@ func deleteUpstream(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
return
}
- // delete from apisix
- request := &service.ApisixUpstreamRequest{Id: uid}
- if _, err := request.Delete(); err != nil {
- e := errno.FromMessage(errno.ApisixUpstreamDeleteError, err.Error())
+ db := conf.DB()
+ tx := db.Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+ // delete from mysql
+ rd := &service.UpstreamDao{}
+ rd.ID = uuid.FromStringOrNil(uid)
+ if err := tx.Delete(rd).Error; err != nil {
+ tx.Rollback()
+ e := errno.FromMessage(errno.DBUpstreamDeleteError, err.Error())
logger.Error(e.Msg)
c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
return
} else {
- // delete from mysql
- rd := &service.UpstreamDao{}
- rd.ID = uuid.FromStringOrNil(uid)
- if err := conf.DB().Delete(rd).Error; err != nil {
- e := errno.FromMessage(errno.DBUpstreamDeleteError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
- return
+ // delete from apisix
+ request := &service.ApisixUpstreamRequest{Id: uid}
+ if _, err := request.Delete(); err != nil {
+ tx.Rollback()
+ if httpError, ok := err.(*errno.HttpError); ok {
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ } else {
+ e := errno.FromMessage(errno.ApisixUpstreamDeleteError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
}
}
+ if err := tx.Commit().Error; err != nil {
+ e := errno.FromMessage(errno.ApisixUpstreamDeleteError, err.Error())
+ logger.Error(e.Msg)
+ }
c.Data(http.StatusOK, service.ContentType, errno.Success())
}
diff --git a/api/route/zclear_test.go b/api/route/zclear_test.go
new file mode 100644
index 0000000000..e44101bd34
--- /dev/null
+++ b/api/route/zclear_test.go
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/apisix/manager-api/service"
+)
+
+func TestClearTestData(t *testing.T) {
+ // delete consumers
+ c1, _ := service.GetConsumerByUserName("e2e_test_consumer1")
+ handler.
+ Delete(uriPrefix + "/consumers/" + c1.ID.String()).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+
+ //delete test ssl
+ service.DeleteTestSslData()
+}
diff --git a/api/run/run.sh b/api/run/run.sh
new file mode 100755
index 0000000000..4bb26dc8b8
--- /dev/null
+++ b/api/run/run.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+pwd=`pwd`
+
+cp ${pwd}/api/conf/conf_preview.json ${pwd}/conf.json
+
+export MYSQL_SERVER_ADDRESS="127.0.0.1:3306"
+export MYSQL_USER=root
+export MYSQL_PASSWORD=123456
+export SYSLOG_HOST=127.0.0.1
+export APISIX_BASE_URL="http://127.0.0.1:9080/apisix/admin"
+export APISIX_API_KEY="edd1c9f034335f136f87ad84b625c8f1"
+
+if [[ "$unamestr" == 'Darwin' ]]; then
+ sed -i '' -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
+ sed -i '' -e "s%#mysqlUser#%`echo $MYSQL_USER`%g" ${pwd}/conf.json
+ sed -i '' -e "s%#mysqlPWD#%`echo $MYSQL_PASSWORD`%g" ${pwd}/conf.json
+ sed -i '' -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
+ sed -i '' -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
+ sed -i '' -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
+else
+ sed -i -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
+ sed -i -e "s%#mysqlUser#%`echo $MYSQL_USER`%g" ${pwd}/conf.json
+ sed -i -e "s%#mysqlPWD#%`echo $MYSQL_PASSWORD`%g" ${pwd}/conf.json
+ sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
+ sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
+ sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
+fi
+
+
+cp ${pwd}/conf.json ${pwd}/api/conf/conf.json
+
+exec ./manager-api
+
diff --git a/api/script/db/schema.sql b/api/script/db/schema.sql
index 83c4bdd611..b1db5a0052 100644
--- a/api/script/db/schema.sql
+++ b/api/script/db/schema.sql
@@ -12,6 +12,7 @@ CREATE TABLE `routes` (
`priority` int NOT NULL DEFAULT 0,
`state` int NOT NULL DEFAULT 1, -- 1-normal 0-disable
`content` text,
+ `script` text,
`content_admin_api` text,
`create_time` bigint(20),
`update_time` bigint(20),
@@ -28,7 +29,9 @@ CREATE TABLE `ssls` (
`status` tinyint(1) unsigned NOT NULL DEFAULT '1',
`create_time` bigint(20) unsigned NOT NULL,
`update_time` bigint(20) unsigned NOT NULL,
- PRIMARY KEY (`id`)
+ `public_key_hash` varchar(64) NOT NULL DEFAULT '',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uni_public_key_hash` (`public_key_hash`)
) DEFAULT CHARSET=utf8;
-- upstream
diff --git a/api/service/consumer.go b/api/service/consumer.go
index a29dde6fa5..98dee452fc 100644
--- a/api/service/consumer.go
+++ b/api/service/consumer.go
@@ -102,8 +102,8 @@ func ConsumerList(page, size int, search string) (int, []ConsumerDto, error) {
db := conf.DB().Table("consumers")
if search != "" {
- db = db.Where("name like ? ", "%"+search+"%").
- Or("description like ? ", "%"+search+"%")
+ db = db.Where("username like ? ", "%"+search+"%").
+ Or("`desc` like ? ", "%"+search+"%")
}
if err := db.Order("create_time desc").Offset((page - 1) * size).Limit(size).Find(&consumerList).Error; err != nil {
@@ -128,6 +128,10 @@ func ConsumerList(page, size int, search string) (int, []ConsumerDto, error) {
}
func ConsumerItem(id string) (*ConsumerDto, error) {
+ if id == "" {
+ return nil, errno.New(errno.InvalidParam)
+ }
+
consumer := &Consumer{}
if err := conf.DB().Table("consumers").Where("id = ?", id).First(consumer).Error; err != nil {
e := errno.New(errno.DBReadError, err.Error())
@@ -144,6 +148,14 @@ func ConsumerCreate(param interface{}, id string) error {
req := &ConsumerDto{}
req.Parse(param)
+ if req.Username == "" {
+ return errno.New(errno.InvalidParamDetail, "username is required")
+ }
+
+ if len(req.Desc) > 200 {
+ return errno.New(errno.InvalidParamDetail, "description is too long")
+ }
+
exists := Consumer{}
conf.DB().Table("consumers").Where("username = ?", req.Username).First(&exists)
if exists != (Consumer{}) {
@@ -151,10 +163,29 @@ func ConsumerCreate(param interface{}, id string) error {
return e
}
+ // trans
+ tx := conf.DB().Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+
+ consumer := &Consumer{}
+ consumer.Transfer(req)
+
+ // update mysql
+ consumer.ID = uuid.FromStringOrNil(id)
+ if err := tx.Create(consumer).Error; err != nil {
+ tx.Rollback()
+ return errno.New(errno.DBWriteError, err.Error())
+ }
+
apisixConsumer := &ApisixConsumer{}
apisixConsumer.Transfer(req)
if _, err := apisixConsumer.PutConsumerToApisix(req.Username); err != nil {
+ tx.Rollback()
if _, ok := err.(*errno.HttpError); ok {
return err
}
@@ -162,33 +193,56 @@ func ConsumerCreate(param interface{}, id string) error {
return e
}
- consumer := &Consumer{}
- consumer.Transfer(req)
-
- // update mysql
- consumer.ID = uuid.FromStringOrNil(id)
- if err := conf.DB().Create(consumer).Error; err != nil {
- return errno.New(errno.DBWriteError, err.Error())
- }
+ tx.Commit()
return nil
}
func ConsumerUpdate(param interface{}, id string) error {
+ if id == "" {
+ return errno.New(errno.InvalidParam)
+ }
+
req := &ConsumerDto{}
req.Parse(param)
+ if req == nil {
+ return errno.New(errno.InvalidParam)
+ }
- exists := Consumer{}
- conf.DB().Table("consumers").Where("username = ?", req.Username).First(&exists)
- if exists != (Consumer{}) && exists.ID != req.ID {
- e := errno.New(errno.DuplicateUserName)
- return e
+ req.ID = uuid.FromStringOrNil(id)
+ if req.Username != "" {
+ exists := Consumer{}
+ conf.DB().Table("consumers").Where("username = ?", req.Username).First(&exists)
+ if exists != (Consumer{}) && exists.ID != req.ID {
+ e := errno.New(errno.DuplicateUserName)
+ return e
+ }
+ }
+ // trans
+ tx := conf.DB().Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+
+ // update mysql
+ consumer := &Consumer{}
+ consumer.Transfer(req)
+ data := Consumer{Desc: consumer.Desc, Plugins: consumer.Plugins}
+ if req.Username != "" {
+ data.Username = req.Username
+ }
+ if err := tx.Model(&consumer).Updates(data).Error; err != nil {
+ tx.Rollback()
+ return errno.New(errno.DBWriteError, err.Error())
}
apisixConsumer := &ApisixConsumer{}
apisixConsumer.Transfer(req)
if _, err := apisixConsumer.PutConsumerToApisix(req.Username); err != nil {
+ tx.Rollback()
if _, ok := err.(*errno.HttpError); ok {
return err
}
@@ -196,18 +250,15 @@ func ConsumerUpdate(param interface{}, id string) error {
return e
}
- // update mysql
- consumer := &Consumer{}
- consumer.Transfer(req)
- consumer.ID = uuid.FromStringOrNil(id)
- if err := conf.DB().Model(&consumer).Updates(consumer).Error; err != nil {
- return errno.New(errno.DBWriteError, err.Error())
- }
+ tx.Commit()
return nil
}
func ConsumerDelete(id string) error {
+ if id == "" {
+ return errno.New(errno.InvalidParam)
+ }
//
consumer := &Consumer{}
if err := conf.DB().Table("consumers").Where("id = ?", id).First(consumer).Error; err != nil {
@@ -215,17 +266,31 @@ func ConsumerDelete(id string) error {
return e
}
+ // trans
+ tx := conf.DB().Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+
+ // delete from mysql
+ if err := conf.DB().Delete(consumer).Error; err != nil {
+ tx.Rollback()
+ return errno.New(errno.DBDeleteError, err.Error())
+ }
+
+ //delete from apisix
if _, err := consumer.DeleteConsumerFromApisix(); err != nil {
+ tx.Rollback()
if _, ok := err.(*errno.HttpError); ok {
return err
}
e := errno.New(errno.ApisixConsumerDeleteError, err.Error())
return e
}
- // delete from mysql
- if err := conf.DB().Delete(consumer).Error; err != nil {
- return errno.New(errno.DBDeleteError, err.Error())
- }
+
+ tx.Commit()
return nil
}
@@ -269,3 +334,16 @@ func (req *Consumer) DeleteConsumerFromApisix() (*ApisixConsumerResponse, error)
}
}
}
+
+func GetConsumerByUserName(name string) (*Consumer, error) {
+ if name == "" {
+ return nil, errno.New(errno.InvalidParam)
+ }
+ consumer := &Consumer{}
+ if err := conf.DB().Table("consumers").Where("username = ?", name).First(consumer).Error; err != nil {
+ e := errno.New(errno.NotFoundError, err.Error())
+ return nil, e
+ }
+
+ return consumer, nil
+}
diff --git a/api/service/consumer_test.go b/api/service/consumer_test.go
index ea1a077aeb..cd0e3d94e2 100644
--- a/api/service/consumer_test.go
+++ b/api/service/consumer_test.go
@@ -125,8 +125,8 @@ func TestConsumerCurd(t *testing.T) {
err = ConsumerDelete(c1.String())
assert.Nil(err)
- count2, _, err := ConsumerList(2, 1, "")
- assert.Equal(count2, count-1)
+ _, err = ConsumerItem(c1.String())
+ assert.Equal(errno.DBReadError.Code, err.(*errno.ManagerError).Code)
err = ConsumerDelete(c2.String())
assert.Nil(err)
diff --git a/api/service/route.go b/api/service/route.go
index ea7e535a02..23bf7864b4 100644
--- a/api/service/route.go
+++ b/api/service/route.go
@@ -19,6 +19,8 @@ package service
import (
"encoding/json"
"fmt"
+ "io/ioutil"
+ "os/exec"
"time"
"github.com/apisix/manager-api/conf"
@@ -75,6 +77,11 @@ func (rd *Route) Parse(r *RouteRequest, arr *ApisixRouteRequest) error {
} else {
rd.Content = string(content)
}
+ if script, err := json.Marshal(r.Script); err != nil {
+ return err
+ } else {
+ rd.Script = string(script)
+ }
timestamp := time.Now().Unix()
rd.CreateTime = timestamp
rd.Priority = r.Priority
@@ -172,6 +179,7 @@ type RouteRequest struct {
UpstreamPath *UpstreamPath `json:"upstream_path,omitempty"`
UpstreamHeader map[string]string `json:"upstream_header,omitempty"`
Plugins map[string]interface{} `json:"plugins"`
+ Script map[string]interface{} `json:"script"`
}
func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
@@ -182,6 +190,9 @@ func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
if o.Upstream != nil && o.Upstream.EnableWebsocket {
protocols = append(protocols, WEBSOCKET)
}
+ if o.UpstreamId != "" {
+ protocols = append(protocols, WEBSOCKET)
+ }
flag := true
for _, t := range o.Vars {
if t[0] == SCHEME {
@@ -221,7 +232,9 @@ func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
upstreamProtocol = pr.Scheme
}
upstreamHeader = pr.Headers
- if pr.RegexUri == nil || len(pr.RegexUri) < 2 {
+ if (pr.RegexUri == nil || len(pr.RegexUri) < 2) && pr.Uri == "" {
+ upstreamPath = nil
+ } else if pr.RegexUri == nil || len(pr.RegexUri) < 2 {
upstreamPath.UPathType = UPATHTYPE_STATIC
upstreamPath.To = pr.Uri
} else {
@@ -243,9 +256,10 @@ func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
//Plugins
requestPlugins := utils.CopyMap(o.Plugins)
delete(requestPlugins, REDIRECT)
+ delete(requestPlugins, PROXY_REWRIETE)
// check if upstream is not exist
- if o.Upstream == nil {
+ if o.Upstream == nil && o.UpstreamId == "" {
upstreamProtocol = ""
upstreamHeader = nil
upstreamPath = nil
@@ -262,6 +276,7 @@ func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
Hosts: o.Hosts,
Redirect: redirect,
Upstream: o.Upstream,
+ UpstreamId: o.UpstreamId,
UpstreamProtocol: upstreamProtocol,
UpstreamPath: upstreamPath,
UpstreamHeader: upstreamHeader,
@@ -314,7 +329,7 @@ func (r Redirect) MarshalJSON() ([]byte, error) {
m := make(map[string]interface{})
if r.HttpToHttps {
m["http_to_https"] = true
- } else {
+ } else if r.Uri != "" {
m["code"] = r.Code
m["uri"] = r.Uri
}
@@ -354,6 +369,7 @@ type ApisixRouteRequest struct {
Upstream *Upstream `json:"upstream,omitempty"`
UpstreamId string `json:"upstream_id,omitempty"`
Plugins map[string]interface{} `json:"plugins,omitempty"`
+ Script string `json:"script,omitempty"`
//Name string `json:"name"`
}
@@ -392,6 +408,7 @@ type Route struct {
UpstreamId string `json:"upstream_id"`
Priority int64 `json:"priority"`
Content string `json:"content"`
+ Script string `json:"script"`
ContentAdminApi string `json:"content_admin_api"`
}
@@ -477,15 +494,16 @@ func ToApisixRequest(routeRequest *RouteRequest) *ApisixRouteRequest {
} else {
arr.Vars = nil
}
-
+ // upstreamId
+ arr.UpstreamId = routeRequest.UpstreamId
// upstream protocol
- if arr.Upstream != nil {
+ if arr.Upstream != nil || arr.UpstreamId != "" {
pr := &ProxyRewrite{}
pr.Scheme = routeRequest.UpstreamProtocol
// upstream path
proxyPath := routeRequest.UpstreamPath
if proxyPath != nil {
- if proxyPath.UPathType == UPATHTYPE_STATIC {
+ if proxyPath.UPathType == UPATHTYPE_STATIC || proxyPath.UPathType == "" {
pr.Uri = proxyPath.To
pr.RegexUri = nil
} else {
@@ -494,7 +512,7 @@ func ToApisixRequest(routeRequest *RouteRequest) *ApisixRouteRequest {
}
// upstream headers
pr.Headers = routeRequest.UpstreamHeader
- if proxyPath != nil {
+ if proxyPath != nil || pr.Scheme != UPATHTYPE_KEEP || (pr.Headers != nil && len(pr.Headers) > 0) {
plugins[PROXY_REWRIETE] = pr
}
}
@@ -504,11 +522,41 @@ func ToApisixRequest(routeRequest *RouteRequest) *ApisixRouteRequest {
} else {
arr.Plugins = nil
}
- // upstreamId
- arr.UpstreamId = routeRequest.UpstreamId
+
+ if routeRequest.Script != nil {
+ arr.Script, _ = generateLuaCode(routeRequest.Script)
+ }
+
return arr
}
+func generateLuaCode(script map[string]interface{}) (string, error) {
+ scriptString, err := json.Marshal(script)
+ if err != nil {
+ return "", err
+ }
+
+ cmd := exec.Command("sh", "-c",
+ "cd /go/manager-api/dag-to-lua/ && lua cli.lua "+
+ "'"+string(scriptString)+"'")
+
+ logger.Info("generate conf:", string(scriptString))
+
+ stdout, _ := cmd.StdoutPipe()
+ defer stdout.Close()
+ if err := cmd.Start(); err != nil {
+ logger.Info("generate err:", err)
+ return "", err
+ }
+
+ result, _ := ioutil.ReadAll(stdout)
+ resData := string(result)
+
+ logger.Info("generated code:", resData)
+
+ return resData, nil
+}
+
func ToRoute(routeRequest *RouteRequest,
arr *ApisixRouteRequest,
u4 uuid.UUID,
@@ -523,14 +571,16 @@ func ToRoute(routeRequest *RouteRequest,
}
rd.ID = u4
// content_admin_api
- if respStr, err := json.Marshal(resp); err != nil {
- e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
- return nil, e
- } else {
- rd.ContentAdminApi = string(respStr)
+ if resp != nil {
+ if respStr, err := json.Marshal(resp); err != nil {
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ return nil, e
+ } else {
+ rd.ContentAdminApi = string(respStr)
+ }
}
// hosts
- hosts := resp.Node.Value.Hosts
+ hosts := routeRequest.Hosts
if hb, err := json.Marshal(hosts); err != nil {
e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
logger.Warn(e.Msg)
@@ -538,7 +588,7 @@ func ToRoute(routeRequest *RouteRequest,
rd.Hosts = string(hb)
}
// uris
- uris := resp.Node.Value.Uris
+ uris := routeRequest.Uris
if ub, err := json.Marshal(uris); err != nil {
e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
logger.Warn(e.Msg)
@@ -546,8 +596,8 @@ func ToRoute(routeRequest *RouteRequest,
rd.Uris = string(ub)
}
// upstreamNodes
- if resp.Node.Value.Upstream != nil {
- nodes := resp.Node.Value.Upstream.Nodes
+ if routeRequest.Upstream != nil {
+ nodes := routeRequest.Upstream.Nodes
ips := make([]string, 0)
for k, _ := range nodes {
ips = append(ips, k)
diff --git a/api/service/route_test.go b/api/service/route_test.go
index 5faacd889f..98bf3e016b 100644
--- a/api/service/route_test.go
+++ b/api/service/route_test.go
@@ -20,6 +20,7 @@ import (
"encoding/json"
"testing"
+ uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
)
@@ -60,6 +61,41 @@ func TestToApisixRequest_RediretPlugins(t *testing.T) {
a.NotEqual(nil, ar.Plugins["redirect"])
}
+func TestToApisixRequest_proxyRewrite(t *testing.T) {
+ nodes := make(map[string]int64)
+ nodes["127.0.0.1:8080"] = 100
+ upstream := &Upstream{
+ UType: "roundrobin",
+ Nodes: nodes,
+ Timeout: UpstreamTimeout{15, 15, 15},
+ }
+ to := "/hello"
+ upstreamPath := &UpstreamPath{To: to}
+ rr := &RouteRequest{
+ ID: "u guess a uuid",
+ Name: "a special name",
+ Desc: "any description",
+ Priority: 0,
+ Methods: []string{"GET"},
+ Uris: []string{},
+ Hosts: []string{"www.baidu.com"},
+ Protocols: []string{"http", "https", "websocket"},
+ Redirect: &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+ Vars: [][]string{},
+ Upstream: upstream,
+ UpstreamPath: upstreamPath,
+ }
+ ar := ToApisixRequest(rr)
+ a := assert.New(t)
+ var pr ProxyRewrite
+ bytes, _ := json.Marshal(ar.Plugins["proxy-rewrite"])
+ json.Unmarshal(bytes, &pr)
+
+ a.Equal(2, len(ar.Plugins))
+ a.NotEqual(nil, ar.Plugins["redirect"])
+ a.Equal(to, pr.Uri)
+}
+
func TestToApisixRequest_Vars(t *testing.T) {
rr := &RouteRequest{
ID: "u guess a uuid",
@@ -168,3 +204,126 @@ func TestApisixRouteResponse_Parse(t *testing.T) {
_, err := arr.Parse()
a.Equal(nil, err)
}
+
+// parse from params to RouteRequest
+func TestRouteRequest_Parse(t *testing.T) {
+ a := assert.New(t)
+ param := []byte(`{
+ "name": "API 名称",
+ "protocols": [
+ "http",
+ "https"
+ ],
+ "hosts": [
+ "www.baidu.com"
+ ],
+ "methods": [
+ "GET",
+ "HEAD",
+ "POST",
+ "PUT",
+ "DELETE",
+ "OPTIONS",
+ "PATCH"
+ ],
+ "redirect": {
+ "code": 302,
+ "uri": "11111"
+ },
+ "vars": [],
+ "script": {
+ "rule": {},
+ "conf": {}
+ }
+ }`)
+ routeRequest := &RouteRequest{}
+ err := routeRequest.Parse(param)
+ a.Nil(err)
+ a.Equal("API 名称", routeRequest.Name)
+ a.Equal(int64(0), routeRequest.Priority)
+ a.Equal(2, len(routeRequest.Script))
+ a.Equal("/*", routeRequest.Uris[0])
+}
+
+// parse from RouteRequest and ApisixRouteRequest to Route
+func TestRoute_Parse(t *testing.T) {
+ a := assert.New(t)
+ rrb := []byte(`{"name":"API 名称2","methods":["GET","HEAD","POST","PUT","DELETE","OPTIONS","PATCH"],"uris":["/*"],"hosts":["www.baidu.com"],"protocols":["http","https"],"redirect":{"code":302,"uri":"11111"},"plugins":null}`)
+ routeRequest := &RouteRequest{}
+ json.Unmarshal(rrb, routeRequest)
+ arrb := []byte(`{"priority":0,"methods":["GET","HEAD","POST","PUT","DELETE","OPTIONS","PATCH"],"uris":["/*"],"hosts":["www.baidu.com"],"plugins":{"redirect":{"code":302,"uri":"11111"}}, "script":{
+ "rule":{
+ "root": "11-22-33-44",
+ "11-22-33-44":[
+ [
+ "code == 503",
+ "yy-uu-ii-oo"
+ ],
+ [
+ "",
+ "vv-cc-xx-zz"
+ ]
+ ]
+ },
+ "conf":{
+ "11-22-33-44":{
+ "name": "limit-count",
+ "conf": {
+ "count":2,
+ "time_window":60,
+ "rejected_code":503,
+ "key":"remote_addr"
+ }
+ },
+ "yy-uu-ii-oo":{
+ "name": "response-rewrite",
+ "conf": {
+ "body":{"code":"ok","message":"request has been limited."},
+ "headers":{
+ "X-limit-status": "limited"
+ }
+ }
+ },
+ "vv-cc-xx-zz":{
+ "name": "response-rewrite",
+ "conf": {
+ "body":{"code":"ok","message":"normal request"},
+ "headers":{
+ "X-limit-status": "normal"
+ }
+ }
+ }
+ }
+} }`)
+ arr := &ApisixRouteRequest{}
+ json.Unmarshal(arrb, arr)
+
+ rd := &Route{}
+ err := rd.Parse(routeRequest, arr)
+ a.Nil(err)
+}
+
+// parse Route
+func TestToRoute(t *testing.T) {
+ a := assert.New(t)
+ b1 := []byte(`{"name":"API 名称2","methods":["GET","HEAD","POST","PUT","DELETE","OPTIONS","PATCH"],"uris":["/*"],"hosts":["www.baidu.com"],"protocols":["http","https"],"redirect":{"code":302,"uri":"11111"},"plugins":null}`)
+ rr := &RouteRequest{}
+ err := json.Unmarshal(b1, &rr)
+ a.Nil(err)
+
+ b2 := []byte(`{"priority":0,"methods":["GET","HEAD","POST","PUT","DELETE","OPTIONS","PATCH"],"uris":["/*"],"hosts":["www.baidu.com"],"plugins":{"redirect":{"code":302,"uri":"11111"}},"script":"function(vars, opts) return vars[\"arg_key\"] == \"a\" or vars[\"arg_key\"] == \"b\" end"}`)
+ arr := &ApisixRouteRequest{}
+ err = json.Unmarshal(b2, &arr)
+ a.Nil(err)
+
+ b3 := []byte(`{"action":"set","node":{"value":{"id":"","name":"","priority":0,"methods":["GET","HEAD","POST","PUT","DELETE","OPTIONS","PATCH"],"uris":["/*"],"hosts":["www.baidu.com"],"vars":null,"plugins":{"redirect":{"code":302,"ret_code":302,"uri":"11111"}}},"modifiedIndex":75}}`)
+ arp := &ApisixRouteResponse{}
+ err = json.Unmarshal(b3, &arp)
+ a.Nil(err)
+
+ u4 := uuid.NewV4()
+ route, err := ToRoute(rr, arr, u4, arp)
+ a.Nil(err)
+ t.Log(route.Uris)
+ a.Equal("[\"/*\"]", route.Uris)
+}
diff --git a/api/service/ssl.go b/api/service/ssl.go
index 08dea43d87..2fbfbfd948 100644
--- a/api/service/ssl.go
+++ b/api/service/ssl.go
@@ -17,12 +17,15 @@
package service
import (
+ "crypto/md5"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
+ "regexp"
+ "strings"
"github.com/apisix/manager-api/conf"
"github.com/apisix/manager-api/errno"
@@ -37,6 +40,7 @@ type Ssl struct {
Snis string `json:"snis"`
Status uint64 `json:"status"`
PublicKey string `json:"public_key,omitempty"`
+ PublicKeyHash string `json:"public_key_hash,omitempty"`
}
type SslDto struct {
@@ -69,8 +73,9 @@ type SslNode struct {
func (req *SslRequest) Parse(body interface{}) {
if err := json.Unmarshal(body.([]byte), req); err != nil {
+ logger.Info("req:")
+ logger.Info(req)
req = nil
- logger.Error(errno.FromMessage(errno.RouteRequestError, err.Error()).Msg)
}
}
@@ -91,7 +96,7 @@ func (sslDto *SslDto) Parse(ssl *Ssl) error {
return nil
}
-func SslList(page, size, status, expireStart, expireEnd int, sni string) (int, []SslDto, error) {
+func SslList(page, size, status, expireStart, expireEnd int, sni, sortType string) (int, []SslDto, error) {
var count int
sslList := []Ssl{}
db := conf.DB().Table("ssls")
@@ -109,7 +114,12 @@ func SslList(page, size, status, expireStart, expireEnd int, sni string) (int, [
db = db.Where("validity_end <= ? ", expireEnd)
}
- if err := db.Order("validity_end desc").Offset((page - 1) * size).Limit(size).Find(&sslList).Error; err != nil {
+ sortType = strings.ToLower(sortType)
+ if sortType != "desc" {
+ sortType = "asc"
+ }
+
+ if err := db.Order("validity_end " + sortType).Offset((page - 1) * size).Limit(size).Find(&sslList).Error; err != nil {
e := errno.New(errno.DBReadError, err.Error())
return 0, nil, e
}
@@ -131,7 +141,11 @@ func SslList(page, size, status, expireStart, expireEnd int, sni string) (int, [
}
func SslItem(id string) (*SslDto, error) {
+ if id == "" {
+ return nil, errno.New(errno.InvalidParam)
+ }
ssl := &Ssl{}
+
if err := conf.DB().Table("ssls").Where("id = ?", id).First(ssl).Error; err != nil {
e := errno.New(errno.DBReadError, err.Error())
return nil, e
@@ -166,53 +180,126 @@ func SslCreate(param interface{}, id string) error {
sslReq := &SslRequest{}
sslReq.Parse(param)
- ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+ if sslReq.PrivateKey == "" {
+ return errno.New(errno.InvalidParamDetail, "Key is required")
+ }
+ if sslReq.PublicKey == "" {
+ return errno.New(errno.InvalidParamDetail, "Cert is required")
+ }
+ sslReq.PublicKey = strings.TrimSpace(sslReq.PublicKey)
+ sslReq.PrivateKey = strings.TrimSpace(sslReq.PrivateKey)
+
+ ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
if err != nil {
e := errno.FromMessage(errno.SslParseError, err.Error())
return e
}
- // first admin api
+ ssl.ID = uuid.FromStringOrNil(id)
+ ssl.Status = 1
+ data := []byte(ssl.PublicKey)
+ hash := md5.Sum(data)
+ ssl.PublicKeyHash = fmt.Sprintf("%x", hash)
+
+ //check hash
+ exists := Ssl{}
+ conf.DB().Table("ssls").Where("public_key_hash = ?", ssl.PublicKeyHash).First(&exists)
+ if exists != (Ssl{}) {
+ e := errno.New(errno.DuplicateSslCert)
+ return e
+ }
+
+ //check sni
var snis []string
_ = json.Unmarshal([]byte(ssl.Snis), &snis)
sslReq.Snis = snis
+ // trans
+ tx := conf.DB().Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+
+ // update mysql
+ if err := tx.Create(ssl).Error; err != nil {
+ tx.Rollback()
+ return errno.New(errno.DBWriteError, err.Error())
+ }
+
+ //admin api
+
if _, err := sslReq.PutToApisix(id); err != nil {
+ tx.Rollback()
if _, ok := err.(*errno.HttpError); ok {
return err
}
e := errno.New(errno.ApisixSslCreateError, err.Error())
return e
}
- // then mysql
- ssl.ID = uuid.FromStringOrNil(id)
- ssl.Status = 1
- if err := conf.DB().Create(ssl).Error; err != nil {
- return errno.New(errno.DBWriteError, err.Error())
- }
+ tx.Commit()
return nil
}
func SslUpdate(param interface{}, id string) error {
+ if id == "" {
+ return errno.New(errno.InvalidParam)
+ }
+
sslReq := &SslRequest{}
sslReq.Parse(param)
- ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+ if sslReq.PrivateKey == "" {
+ return errno.New(errno.InvalidParamDetail, "Key is required")
+ }
+ if sslReq.PublicKey == "" {
+ return errno.New(errno.InvalidParamDetail, "Cert is required")
+ }
+ ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
if err != nil {
- e := errno.FromMessage(errno.SslParseError, err.Error())
+ return errno.FromMessage(errno.SslParseError, err.Error())
+ }
+
+ hash := md5.Sum([]byte(ssl.PublicKey))
+ ssl.ID = uuid.FromStringOrNil(id)
+ ssl.PublicKeyHash = fmt.Sprintf("%x", hash)
+
+ //check hash
+ exists := Ssl{}
+ conf.DB().Table("ssls").Where("public_key_hash = ?", ssl.PublicKeyHash).First(&exists)
+ if exists != (Ssl{}) && exists.ID != ssl.ID {
+ e := errno.New(errno.DuplicateSslCert)
return e
}
- // first admin api
+ // trans
+ tx := conf.DB().Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+
+ //sni check
var snis []string
_ = json.Unmarshal([]byte(ssl.Snis), &snis)
sslReq.Snis = snis
+ // update mysql
+ data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart: ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
+ if err := tx.Model(&ssl).Updates(data).Error; err != nil {
+ tx.Rollback()
+ return errno.New(errno.DBWriteError, err.Error())
+ }
+
+ //admin api
if _, err := sslReq.PutToApisix(id); err != nil {
+ tx.Rollback()
if _, ok := err.(*errno.HttpError); ok {
return err
}
@@ -220,21 +307,37 @@ func SslUpdate(param interface{}, id string) error {
return e
}
- // then mysql
- ssl.ID = uuid.FromStringOrNil(id)
- data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart: ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
- if err := conf.DB().Model(&ssl).Updates(data).Error; err != nil {
- return errno.New(errno.DBWriteError, err.Error())
- }
+ tx.Commit()
return nil
}
func SslPatch(param interface{}, id string) error {
+ if id == "" {
+ return errno.New(errno.InvalidParam)
+ }
+
sslReq := &SslRequest{}
sslReq.Parse(param)
+ // trans
+ tx := conf.DB().Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+
+ // update mysql
+ ssl := Ssl{}
+ ssl.ID = uuid.FromStringOrNil(id)
+ if err := tx.Model(&ssl).Update("status", sslReq.Status).Error; err != nil {
+ tx.Rollback()
+ return errno.New(errno.DBWriteError, err.Error())
+ }
+
if _, err := sslReq.PatchToApisix(id); err != nil {
+ tx.Rollback()
if _, ok := err.(*errno.HttpError); ok {
return err
}
@@ -242,32 +345,45 @@ func SslPatch(param interface{}, id string) error {
return e
}
- ssl := Ssl{}
- ssl.ID = uuid.FromStringOrNil(id)
- if err := conf.DB().Model(&ssl).Update("status", sslReq.Status).Error; err != nil {
- return errno.New(errno.DBWriteError, err.Error())
- }
+ tx.Commit()
return nil
}
func SslDelete(id string) error {
+ if id == "" {
+ return errno.New(errno.InvalidParam)
+ }
+
+ // trans
+ tx := conf.DB().Begin()
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ }
+ }()
+
+ // delete from mysql
+ ssl := &Ssl{}
+ ssl.ID = uuid.FromStringOrNil(id)
+ if err := conf.DB().Delete(ssl).Error; err != nil {
+ tx.Rollback()
+ return errno.New(errno.DBDeleteError, err.Error())
+ }
+
// delete from apisix
request := &SslRequest{}
request.ID = id
if _, err := request.DeleteFromApisix(); err != nil {
+ tx.Rollback()
if _, ok := err.(*errno.HttpError); ok {
return err
}
e := errno.New(errno.ApisixSslDeleteError, err.Error())
return e
}
- // delete from mysql
- ssl := &Ssl{}
- ssl.ID = uuid.FromStringOrNil(id)
- if err := conf.DB().Delete(ssl).Error; err != nil {
- return errno.New(errno.DBDeleteError, err.Error())
- }
+
+ tx.Commit()
return nil
}
@@ -404,3 +520,70 @@ func ParseCert(crt, key string) (*Ssl, error) {
return &ssl, nil
}
}
+
+func CheckSniExists(param interface{}) error {
+ var hosts []string
+ if err := json.Unmarshal(param.([]byte), &hosts); err != nil {
+ return errno.FromMessage(errno.InvalidParam)
+ }
+
+ sslList := []Ssl{}
+ db := conf.DB().Table("ssls")
+ db = db.Where("`status` = ? ", 1)
+
+ condition := ""
+ args := []interface{}{}
+ first := true
+ for _, host := range hosts {
+ idx := strings.Index(host, "*")
+ keyword := strings.Replace(host, "*.", "", -1)
+ if idx == -1 {
+ if j := strings.Index(host, "."); j != -1 {
+ keyword = host[j:]
+ //just one `.`
+ if j := strings.Index(host[(j+1):], "."); j == -1 {
+ keyword = host
+ }
+ }
+ }
+ if first {
+ condition = condition + "`snis` like ?"
+ } else {
+ condition = condition + " or `snis` like ?"
+ }
+ first = false
+ args = append(args, "%"+keyword+"%")
+ }
+ db = db.Where(condition, args...)
+
+ if err := db.Find(&sslList).Error; err != nil {
+ return errno.FromMessage(errno.SslForSniNotExists, hosts[0])
+ }
+
+hre:
+ for _, host := range hosts {
+ for _, ssl := range sslList {
+ sslDto := SslDto{}
+ sslDto.Parse(&ssl)
+ for _, sni := range sslDto.Snis {
+ if sni == host {
+ continue hre
+ }
+ regx := strings.Replace(sni, ".", `\.`, -1)
+ regx = strings.Replace(regx, "*", `([^\.]+)`, -1)
+ regx = "^" + regx + "$"
+ if isOk, _ := regexp.MatchString(regx, host); isOk {
+ continue hre
+ }
+ }
+ }
+ return errno.FromMessage(errno.SslForSniNotExists, host)
+ }
+
+ return nil
+}
+
+func DeleteTestSslData() {
+ db := conf.DB().Table("ssls")
+ db.Where("snis LIKE ? OR (snis LIKE ? AND snis LIKE ? )", "%*.route.com%", "%r.com%", "%s.com%").Delete(Ssl{})
+}
diff --git a/api/service/ssl_test.go b/api/service/ssl_test.go
index 46607e8258..2134bb1be7 100644
--- a/api/service/ssl_test.go
+++ b/api/service/ssl_test.go
@@ -218,6 +218,16 @@ func TestSslCurd(t *testing.T) {
assert.Equal(true, strings.Contains(dm, "test3.com"))
}
+ //test3.com duplicate
+ param = []byte(`{
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIEWjCCAsKgAwIBAgIRAMLLNCKEvgEQL22Hpox6E1kwDQYJKoZIhvcNAQELBQAw\nfzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSowKAYDVQQLDCFqdW54\ndWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikxMTAvBgNVBAMMKG1rY2VydCBq\ndW54dWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikwHhcNMTkwNjAxMDAwMDAw\nWhcNMzAwNjA5MTA0MjA1WjBVMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQg\nY2VydGlmaWNhdGUxKjAoBgNVBAsMIWp1bnh1Y2hlbkBqdW54dWRlQWlyIChqdW54\ndSBjaGVuKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2lg+vzHkRI\nCs8PHv6UVxXFbrL6wlsdurOICkW5daKUJyzUZQZl11CK9SWOk8vAc7j3pQ7Mz15r\nhfQB558WHzI/XXbZ1NrZrTpLaL0fWW5n4hIE8EbYf3Hy/xM8gRUXsWMEexq2WC/R\nPfTCQIZ85vUSANS72E5rHdba3Y5IMr8bn/NUg1sm2LxZQmZV6tBOpYnibyj7bXxw\n8kxr4w+B/5jDBPmwL59bdoatEs1FjdHzz+fbW1K4NdHZZEotYqkQhCS09JnwGswd\nAriy+Is44kt/gtw7nVWmuV/eQaxPEHVE4Bwvdmv11IsPsj6hif23gXjXLIHx66CY\n/S4I/P0allkCAwEAAaN7MHkwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsG\nAQUFBwMBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUVPhGyY/h6etoAqzb298k\nJGJzx1wwIwYDVR0RBBwwGoIJdGVzdDMuY29tgg13d3cudGVzdDMuY29tMA0GCSqG\nSIb3DQEBCwUAA4IBgQCgb5wOMzkD1/tgrjwE7Cj1NvX7/p/JIQVVtvtnnypyXGNn\nVL+q4oB9WzvOvMcTCiKKHqg9jCiu/KFFHy7nRzj7KPhU/o9M7qLNwLJjfUOtPYUm\nrS62kEXlj5L5+UJjiGABGfLllxMwwTkAFbdSUSB1awzoafPn5+g+qABXgkF/EN2I\n+IJcJCqg3IO30n4MMhqNx3IbqIohD3p5GzjQqnuQSrC/HJEsUuIlMCHPJ1GVbbrd\nRnSySMcbv2jThP53JVIe+0HHvcujb2pDQ4RcCSaN3OXaZDYVqoSR04+amotGwiWO\nDY/4LTWFJkfoWnv1kg2/AllMpsXB+1u+O+x6qWzBw2hXP5AM+8KIoJ2/Mb13TsN9\nqhrGep+SfhjARH068ZjaS2zQC2Uvc+SrEGXfITPIkstRELxIV9Lmjl9lwpAOJrte\n4TDjYhBS20j6mt3dUyEBnPfkpcOeLYZNS1sK66MRfzJ9MowdTcWyvMzFHjSjfWPY\n+4scQdmkFkCulnzylak=\n-----END CERTIFICATE-----",
+ "key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9pYPr8x5ESArP\nDx7+lFcVxW6y+sJbHbqziApFuXWilCcs1GUGZddQivUljpPLwHO496UOzM9ea4X0\nAeefFh8yP1122dTa2a06S2i9H1luZ+ISBPBG2H9x8v8TPIEVF7FjBHsatlgv0T30\nwkCGfOb1EgDUu9hOax3W2t2OSDK/G5/zVINbJti8WUJmVerQTqWJ4m8o+218cPJM\na+MPgf+YwwT5sC+fW3aGrRLNRY3R88/n21tSuDXR2WRKLWKpEIQktPSZ8BrMHQK4\nsviLOOJLf4LcO51Vprlf3kGsTxB1ROAcL3Zr9dSLD7I+oYn9t4F41yyB8eugmP0u\nCPz9GpZZAgMBAAECggEAZ2+8SVgb/QASLSdBL3d3HB/IJgSRNyM67qrXd3oVCCyo\nRVI/G8M2Me7okKh4QhxgwdUIiM76l7Qrpo/XZjSppT1cW/Opngg17GKu6OANZiNw\n8YUSDIIO2PbBWxuYCAoZLTmHb2VfKg2FLlc43GGJksdT/rPJ4dOYvdQ4HV+Rlhuo\ntLJznoTStoX+DXLaNU7+jK1ZjbjOKPWcTJt42a0Rvu34ghtbqhkHt9VAvqysgysq\n2GO6Idvu3CE3rgDyde4SBiZL5twtLX3/56daIOG/Gt9NtfvftIilgrPoMfY8WsJH\njY5AaJKaq88a+A2M3ICOAehquhaGqest9kXP5qWzZQKBgQDIrvROSvKxYw9nAksR\nQtvA8fDp6JYxViDqAK5O3kCs0eamCNGycqEOO8yBakWY3YlVxVZVUd/0kymhePEw\nwiJbIZppOaS7xDtdrPvk3QwIVCDYYfZ7SyY1n1FPynpWAYTqIm1YuhWDqXOcQZcy\nxaK86LOtAIAIXomefJYEvkya4wKBgQDx68EKTqxdETiUGUJVxgTLg96Y5zMGLgGO\n6lkqpbUQFfN4yZSHv97P9gugv9fKustGxCEvA9StC2Dq7AtpjOqfYabqiM7ywqzf\nmKazBcIch/qPijVHZO6bLRBUXZWhV7/qZzxN7luenr+U4XwtxBXApGdUmkQYwPnk\nc52J156ikwKBgGOmtLu37cF13i0Zb2s31uV9flK4YvRGv3tTMTsKk/T9GdoyoOZK\nk3z85rUQr1SUFWEY56DgUiQhe1eqNaIvlF3KVuGPdSSj8ZK3ljF0LkhodhLcukdI\n7sVLwlWrxom0oWqeA8w+QvapCzZ5P3o/t2q05puuluURBKdFWD0svd9fAoGANjnk\nAU11MT9E8V1gEx3ZwUyDvr5EH6R8UO6Sog6WsU5aTr7QfkUxymeaX6Pg2N5Z5jjc\nP0+agldEmCPkwvoFNUiMQ5H64UtluJDc/M/TnNWWAkq2epRTL5FAUcjQW2Px7rbJ\nO6ar/rgStWp9jTygq5euWbZigTHwUZbgvx8HveUCgYBsNapwmK2w66ve6rhCHR4W\naNSZyHq/hz84hwhUwSMFv0qU54oJchZWGPnBBIHWlV3J/yYTxQ3p9UWRlH3u2kpQ\nhEcGHWmVjKCc1IL/Qczw4U4koMNSOY+uqJmomOGkDpA2yWuOIDhFXebLSdcJNr0u\n2DYf16scbug3YbbqdD2WGg==\n-----END PRIVATE KEY-----"
+ }`)
+
+ err = SslCreate(param, u2.String())
+ assert.NotNil(err)
+ assert.Equal(errno.DuplicateSslCert.Code, err.(*errno.ManagerError).Code)
+
//a.com b.com fail
param = []byte(`{
"cert": "-----BEGIN CERTIFICATE-----\nMIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD\nTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl\neTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv\nbTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0\nWjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5\nMQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t\nMQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZIhvcNAQEB\nBQADgY0AMIGJAoGBANHMrKlfFzJbyYuD0YveK2mOOXR9zXi+vC5lW6RaoyKjx5AL\nyIXQWXURGVnxw1+xbmxWN1MXZyAP7eJYFPa0PIJvW0kbyHkJt/TrCyBLVOqpTqvE\nkDAIde9Fx83556sXD43Oq93lyBraXmR+fXuoLxJQQLhALW1tOg1X3VrxKYXNAgMB\nAAEwDQYJKoZIhvcNAQELBQADgYEAwJ7qV0Tj6JXR035ySVSBG1KBF19DVmMYRKdO\nSAU1j437q+ktTcEWSA0CkH6rg53tP4V1h0tzdhCxisivYynngjtEcZfsrwdIrsSg\ncmOBZ+KTRyZ2fLgH4F8Naz5hBrwmR8ZIG46feVOV/swJzz4BNaXGj1oATWkLMA3c\nSf0G+aI=\n-----END CERTIFICATE-----",
@@ -238,10 +248,29 @@ func TestSslCurd(t *testing.T) {
assert.Nil(err)
//list
- count, list, err := SslList(2, 1, -1, 0, 0, "")
+ count, list, err := SslList(2, 1, -1, 0, 0, "", "asc")
assert.Equal(true, count >= 2)
assert.Equal(1, len(list))
+ // check sni ssl exist
+ param = []byte(`[
+ "test3.com",
+ "www.test3.com",
+ "a.com"
+ ]`)
+
+ err = CheckSniExists(param)
+ assert.Nil(err)
+
+ // check sni ssl exist
+ param = []byte(`[
+ "test3.com",
+ "a.test3.com",
+ "b.com"
+ ]`)
+ err = CheckSniExists(param)
+ assert.NotNil(err)
+
// patch
param = []byte(`{
"status": 0
@@ -252,24 +281,71 @@ func TestSslCurd(t *testing.T) {
ssl, err = SslItem(u1.String())
assert.Equal(uint64(0), ssl.Status)
+ // check sni ssl exist --- disable test3
+ param = []byte(`[
+ "test3.com",
+ "www.test3.com",
+ "a.com"
+ ]`)
+
+ err = CheckSniExists(param)
+ assert.NotNil(err)
+
+ param = []byte(`[
+ "a.com"
+ ]`)
+ err = CheckSniExists(param)
+ assert.Nil(err)
+
//update
param = []byte(`{
- "cert": "-----BEGIN CERTIFICATE-----\nMIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD\nTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl\neTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv\nbTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0\nWjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5\nMQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t\nMQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZIhvcNAQEB\nBQADgY0AMIGJAoGBANHMrKlfFzJbyYuD0YveK2mOOXR9zXi+vC5lW6RaoyKjx5AL\nyIXQWXURGVnxw1+xbmxWN1MXZyAP7eJYFPa0PIJvW0kbyHkJt/TrCyBLVOqpTqvE\nkDAIde9Fx83556sXD43Oq93lyBraXmR+fXuoLxJQQLhALW1tOg1X3VrxKYXNAgMB\nAAEwDQYJKoZIhvcNAQELBQADgYEAwJ7qV0Tj6JXR035ySVSBG1KBF19DVmMYRKdO\nSAU1j437q+ktTcEWSA0CkH6rg53tP4V1h0tzdhCxisivYynngjtEcZfsrwdIrsSg\ncmOBZ+KTRyZ2fLgH4F8Naz5hBrwmR8ZIG46feVOV/swJzz4BNaXGj1oATWkLMA3c\nSf0G+aI=\n-----END CERTIFICATE-----",
- "key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDRzKypXxcyW8mLg9GL3itpjjl0fc14vrwuZVukWqMio8eQC8iF\n0Fl1ERlZ8cNfsW5sVjdTF2cgD+3iWBT2tDyCb1tJG8h5Cbf06wsgS1TqqU6rxJAw\nCHXvRcfN+eerFw+Nzqvd5cga2l5kfn17qC8SUEC4QC1tbToNV91a8SmFzQIDAQAB\nAoGBAJIL/y4wqf8+ckES1G6fjG0AuvJjGQQzEuDhYjg5eFMG3EdkTIUKkxuxeYpp\niG43H/1+zyiipAFn1Vu5oW5T7cJEgC1YA39dERT605S5BrNWWHoZsgH+qmLoq7X+\njXMlmCagwlgwhUWMU2M1/LUbAl42384dK9u3EwcCgS//sFuBAkEA6mK52/Z03PB3\n0sS14eN7xFl96yc/NcneJ7Vy5APT0KGLo0j2S8gpOVW9EYrrzDzWgQ8FLIeed2Zw\nZ4ATksgRXQJBAOUlh5VJkyMdMiDEeJgK9QKtJkuiLZFAzZiWAUqjvSG2j8tWX/iN\nveI1sXCPyQSKoWPN74+23KWL+nW+mUzkzzECQFf+UIB/+keoD5QVPaNcX+7LGjba\nOSTccIa/3C42MaM1wtK+ZZj1wGRCCAU5/mRiwrUZCnw5PgjdcH2q265TZhECQASY\nJgnGOd8AXNrvVYOm5JazJgtqKwO4iua+SzRV6Bre8C8hgjcXkHESpoYdO+iNZwL7\nRAxbnDzte44UzjoOdGECQGtkrBffiyMaQv6LM/6Fa5TXHb1kPtLGIjFSygR3eTYI\ngHG78R5ac0dzhbyKaOo6cbj7CJVkbBh4BNW94tBZE/I=\n-----END RSA PRIVATE KEY-----"
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIEWzCCAsOgAwIBAgIQDYoN+el2w074sSGlyKVZFTANBgkqhkiG9w0BAQsFADB/\nMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKjAoBgNVBAsMIWp1bnh1\nY2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTExMC8GA1UEAwwobWtjZXJ0IGp1\nbnh1Y2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTAeFw0xOTA2MDEwMDAwMDBa\nFw0zMDA3MDQwNjA0MzNaMFUxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj\nZXJ0aWZpY2F0ZTEqMCgGA1UECwwhanVueHVjaGVuQGp1bnh1ZGVBaXIgKGp1bnh1\nIGNoZW4pMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy21LYFmQYpXm\nMlUjybwbJ338mwKmxY+wTEJLBhw7tBcau9aFjjyO4aRph4rpdMQgCn4lwTME2lbF\ndRhHzU5+Sy9JsI1k+9/J8sZSaTIj9paOX2PYnEOoFjIx9wpJRpeLNBjy3ICS3HC4\nSzTbDVAk9LZILLv/81vt1KpQ1HoPpE+OZ1wX+CL0/6RnNmdaqgrmttPv0sul9yIe\nKz19Hr26px7g6UnoK0o8rSwCqVmjoVTJ+eY2zmmzShqFPTLFgvNZmMeL3dPmg6nG\ndjjsbkXS50thyTDb/h+YIvEsZfrDVgbV2g6P/KiyfKCyevMQxd1J5/UI4HltI3MX\ngrpoaiDiVwIDAQABo30wezAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB\nBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRU+EbJj+Hp62gCrNvb3yQk\nYnPHXDAlBgNVHREEHjAcggtleGFtcGxlLmNvbYINKi5leGFtcGxlLmNvbTANBgkq\nhkiG9w0BAQsFAAOCAYEAuKgNfkAA6uKoAgtQhE4+MPnRmWnHrYaUcqxVYXJZfFyi\nScPaolku+0MsSr1dD2JrbqKMwa54C293e2jkz1EETYKT4bhETS7ttzO1WubLHOyT\nRWb26DVZQH+tPrvUYE4kYSdT3uGi3JNJse2Lpw014nkcwgxOI1Sn8hnfeE7rZU5B\nv79EoqjSwvFDf8aOJTh6mBoe134s3PqrmEjx0VrTlZCkSy9J9REQKkdWmTwW68C2\nrdhV9+/E+xS10WlmxsbGhPgcEhMP0EfGLZm0dh7XUIt06Y3V1iCVFu6/7wQbmO6a\nrvOf2wmoUuZCfZDsLFBc1RIM5AvPktbZJrrhDunum+Sh3Pg8ntDhGSFJM8lCyvo3\n1bQ2rTc0fQJd95ztesaCUdyGi07coLtr1kXPpcv9DLTthXoDltSWB2jTDOPRS902\nzlBfhOmp1H7Xh9OQzEGxJLrtUOntdOM1Ws/GOvopaHdNic7xsodKqlxlnafXTJKA\n7/0x3XEWbPHxrfqvBoax\n-----END CERTIFICATE-----",
+ "key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLbUtgWZBileYy\nVSPJvBsnffybAqbFj7BMQksGHDu0Fxq71oWOPI7hpGmHiul0xCAKfiXBMwTaVsV1\nGEfNTn5LL0mwjWT738nyxlJpMiP2lo5fY9icQ6gWMjH3CklGl4s0GPLcgJLccLhL\nNNsNUCT0tkgsu//zW+3UqlDUeg+kT45nXBf4IvT/pGc2Z1qqCua20+/Sy6X3Ih4r\nPX0evbqnHuDpSegrSjytLAKpWaOhVMn55jbOabNKGoU9MsWC81mYx4vd0+aDqcZ2\nOOxuRdLnS2HJMNv+H5gi8Sxl+sNWBtXaDo/8qLJ8oLJ68xDF3Unn9QjgeW0jcxeC\numhqIOJXAgMBAAECggEARdPea9RSm4SY3+4ZusW3DHdSnmLqnCYWfhbDafWHCOpl\nYtTgQ1LGOO4Wy1ADkvE/jlp+2zKIF+pxHSCYhQDMmUJCKThf0ZWf3JX28+UiMyK6\n/ENptzoCGJxiSkpdnL2oKtnlg4se3kxS9n3OM2OvS9DGNZTS5tZHbRiJZmX/uIZ4\n5JsETflo/emPmH3NJNMmUr+uLtowqn3KT0tCm2nPZUgtSepztTK5ugumk+Apyhc8\n3bno20b+97IzzUmn584C3fIv776rOgyMQPi2CrdtCh1jsgqsO87DyJTWf/BdbqPU\nKFD9buv21vgyCVBUPUHL/RLRkG5iwx5713bExmLjAQKBgQD+UrYu135kqZzgjkQ6\nviC1xUcJgi9mVI1AI7YXUEqx3/IDCr+oum95zDMJW2I5TbWJNdXQ1JmWqX1p06G7\nLDel/lYe/GUjNu76U7eSPL2H29MqymeT+y5GB63U5BqgfVKj39gTlrLoMwz7KF/x\nXtH5z/ZTo82NII12zUX36bbIiwKBgQDMxKv/94T8OWoO067lQ07vVCruhmjojJFa\na8oyv73tS3V8bzP4b1MgUinDaksyfpSdBbd4Tnr3ImvQxEUOJVK0Mg43sTK8GXpF\nPn0VsY+rhynDv0KQZ4GHvE2Mb/QxycJdiaF7TowevNe12Ujc+NGr35CRJhgspgYM\nnNhCppA65QKBgQCztvEEYsTvDyhLSl0OgaINsK1FG9iw4Bi8dT/Mc7GExnJ3EdZj\nvfLeR5zdBNWBFtescP15x3INFBIKgUEtSc69Ht/un348hyoSfKwgy4lHAuDSwRq2\naG3HkM+Wu+XQ+R43rQs8tGYSTVjj9iDuKIoKlJlFe1/aVWGBzQafbGj8hwKBgDLO\nlbLEMo32nPci1OFzyvEdHC3k0cDpp+McnaXr528qavM+EFITJTf+yvf+trvHpo4z\nbet+5YnOU5wQJuY0oomtZdOxttnvJGRr9dNdJD22Ism7+gMke4I3WbJ/0MJNwlk9\nHgEfYyr5RjiLukWBw1x280Lghd0GMLgObqZS97R1AoGAZnB0oCBKrgR1i76jUoXu\nsiSrId5KvcbtuE0H8JMN3rl+77HEYAyJXGRU45e1kyX1HvErGz2Q3mvpFFJxNRf7\nZRwIJh/rEQ/G7svCu6k+5UGt88dbxgtR8C0WfMnmQH0ZW8XLgD6J+R7A7N2vtrd1\nzlzZL/qYUmm8QEK8UN9LbiM=\n-----END PRIVATE KEY-----"
}`)
err = SslUpdate(param, u1.String())
assert.Nil(err)
ssl, _ = SslItem(u1.String())
- assert.Equal(3, len(ssl.Snis))
+ assert.Equal(2, len(ssl.Snis))
+
+ // check sni ssl exist
+ param = []byte(`[
+ "example.com",
+ "www.example.com",
+ "a.example.com",
+ "a.com",
+ "b.com"
+ ]`)
+
+ err = CheckSniExists(param)
+ assert.NotNil(err)
+ assert.Equal(errno.SslForSniNotExists.Code, err.(*errno.ManagerError).Code)
+
+ param = []byte(`{
+ "status": 1
+ }`)
+ err = SslPatch(param, u1.String())
+ assert.Nil(err)
+
+ // check sni ssl exist
+ param = []byte(`[
+ "example.com",
+ "www.example.com",
+ "a.example.com",
+ "a.com",
+ "b.com"
+ ]`)
+
+ err = CheckSniExists(param)
+ assert.Nil(err)
//delete
err = SslDelete(u1.String())
assert.Nil(err)
- count2, _, err := SslList(2, 1, -1, 0, 0, "")
- assert.Equal(count2, count-1)
+ _, err = SslItem(u1.String())
+ assert.Equal(errno.DBReadError.Code, err.(*errno.ManagerError).Code)
err = SslDelete(u2.String())
assert.Nil(err)
diff --git a/api/service/upstream.go b/api/service/upstream.go
index 0302ea9c18..5c41979220 100644
--- a/api/service/upstream.go
+++ b/api/service/upstream.go
@@ -1,3 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package service
import (
@@ -72,6 +89,20 @@ type UpstreamResponse struct {
Upstream
}
+type UpstreamNameResponse struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
+
+func (u *UpstreamDao) Parse2NameResponse() (*UpstreamNameResponse, error) {
+ // upstream
+ unr := &UpstreamNameResponse{
+ ID: u.ID.String(),
+ Name: u.Name,
+ }
+ return unr, nil
+}
+
func (u *UpstreamDao) Parse2Response() (*UpstreamResponse, error) {
// upstream
aur := &ApisixUpstreamResponse{}
@@ -153,11 +184,13 @@ func Trans2UpstreamDao(resp *ApisixUpstreamResponse, r *UpstreamRequest) (*Upstr
u.Content = string(content)
}
// content_admin_api
- if respStr, err := json.Marshal(resp); err != nil {
- e := errno.FromMessage(errno.DBUpstreamError, err.Error())
- return nil, e
- } else {
- u.ContentAdminApi = string(respStr)
+ if resp != nil {
+ if respStr, err := json.Marshal(resp); err != nil {
+ e := errno.FromMessage(errno.DBUpstreamError, err.Error())
+ return nil, e
+ } else {
+ u.ContentAdminApi = string(respStr)
+ }
}
return u, nil
}
diff --git a/compose/README.md b/compose/README.md
index 7d7c7ce30b..98bce58d53 100644
--- a/compose/README.md
+++ b/compose/README.md
@@ -1,56 +1,35 @@
+
+
+# docker-compose
+
+This folder stores the `docker-compose` file for `manager-api`.
+
## Deploy
-```sh
-$ cd incubator-apisix-dashboard/compose
+1. Run docker-compose
-$ chmod +x ./manager_conf/build.sh
+```sh
+$ cd apisix-dashboard/compose
+# For most users in China, please use some proxy services like https://www.daocloud.io/mirror to speed up your Docker images pulling.
$ docker-compose -p dashboard up -d
```
-## Usage
-
-### 1. login dashboard
-
-Visit `http://127.0.0.1/dashboard/` in the browser,
-Enter `http://127.0.0.1:8080/apisix/admin` into the first input box, this is the backend management service address
-
-
-
-now, click `save`.
-
-### 2. If you want to display the grafana metric dashboard, please fill in the grafana shared link as follows
-
-1.get grafana shared link
-
-Visit `http://127.0.0.1:3000/?search=open&orgId=1`
-
-
-
-click `Apache APISIX` dashboard, and you can see the page as follow
-
-
-
-click the button `shard dashboard` on the right of `Apache APISIX`
-
-
-
-copy the link, and then return to dashboard on the step 1
-
-
-
-click metric on the left, and then the config button
-
-Paste shared link
-
-
-
-save, and you can see the metrics
-
-
-
-
-
-
-
-
+2. Visit `http://127.0.0.1/` in the browser.
diff --git a/compose/apisix_conf/config.yaml b/compose/apisix_conf/config.yaml
index bbdc65b95d..3fb47af261 100644
--- a/compose/apisix_conf/config.yaml
+++ b/compose/apisix_conf/config.yaml
@@ -1,49 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# PLEASE DO NOT UPDATE THIS FILE!
+# If you want to set the specified configuration value, you can set the new
+# value in the conf/config.yaml file.
+#
apisix:
- node_listen: 9080 # APISIX listening port
- enable_heartbeat: true
- enable_admin: true
- enable_admin_cors: true # Admin API support CORS response headers.
- enable_debug: false
- enable_dev_mode: false # Sets nginx worker_processes to 1 if set to true
- enable_reuseport: true # Enable nginx SO_REUSEPORT switch if set to true.
- enable_ipv6: true
- config_center: etcd # etcd: use etcd to store the config value
- # yaml: fetch the config value from local yaml file `/your_path/conf/apisix.yaml`
-
- #proxy_protocol: # Proxy Protocol configuration
- # listen_http_port: 9181 # The port with proxy protocol for http, it differs from node_listen and port_admin.
- # This port can only receive http request with proxy protocol, but node_listen & port_admin
- # can only receive http request. If you enable proxy protocol, you must use this port to
- # receive http request with proxy protocol
- # listen_https_port: 9182 # The port with proxy protocol for https
- # enable_tcp_pp: true # Enable the proxy protocol for tcp proxy, it works for stream_proxy.tcp option
- # enable_tcp_pp_to_upstream: true # Enables the proxy protocol to the upstream server
-
- proxy_cache: # Proxy Caching configuration
- cache_ttl: 10s # The default caching time if the upstream does not specify the cache time
- zones: # The parameters of a cache
- - name: disk_cache_one # The name of the cache, administrator can be specify
- # which cache to use by name in the admin api
- memory_size: 50m # The size of shared memory, it's used to store the cache index
- disk_size: 1G # The size of disk, it's used to store the cache data
- disk_path: "/tmp/disk_cache_one" # The path to store the cache data
- cache_levels: "1:2" # The hierarchy levels of a cache
- # - name: disk_cache_two
- # memory_size: 50m
- # disk_size: 1G
- # disk_path: "/tmp/disk_cache_two"
- # cache_levels: "1:2"
-
-# allow_admin: # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow
-# - 127.0.0.0/24 # If we don't set any IP list, then any IP access is allowed by default.
-# - 172.17.0.0/24
- # - "::/64"
- # port_admin: 9180 # use a separate port
-
- # Default token when use API to call for Admin API.
- # *NOTE*: Highly recommended to modify this value to protect APISIX's Admin API.
- # Disabling this configuration item means that the Admin API does not
- # require any authentication.
+ allow_admin:
+ - 0.0.0.0/0
admin_key:
-
name: "admin"
@@ -52,86 +29,12 @@ apisix:
# viewer: only can view configuration data
-
name: "viewer"
- key: 4054f7cf07e344346cd3f287985e76a2
+ key: 4054f7cf07e344346cd3f287985e76a1
role: viewer
- router:
- http: 'radixtree_uri' # radixtree_uri: match route by uri(base on radixtree)
- # radixtree_host_uri: match route by host + uri(base on radixtree)
- ssl: 'radixtree_sni' # radixtree_sni: match route by SNI(base on radixtree)
- # stream_proxy: # TCP/UDP proxy
- # tcp: # TCP proxy port list
- # - 9100
- # - 9101
- # udp: # UDP proxy port list
- # - 9200
- # - 9211
- dns_resolver: # default DNS resolver, with disable IPv6 and enable local DNS
- - 127.0.0.11
- - 114.114.114.114
- - 223.5.5.5
- - 1.1.1.1
- - 8.8.8.8
- dns_resolver_valid: 30 # valid time for dns result 30 seconds
- resolver_timeout: 5 # resolver timeout
- ssl:
- enable: true
- enable_http2: true
- listen_port: 9443
- ssl_protocols: "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"
- ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA"
-
-nginx_config: # config for render the template to genarate nginx.conf
- error_log: "logs/error.log"
- error_log_level: "warn" # warn,error
- worker_rlimit_nofile: 20480 # the number of files a worker process can open, should be larger than worker_connections
- event:
- worker_connections: 10620
- http:
- access_log: "logs/access.log"
- keepalive_timeout: 60s # timeout during which a keep-alive client connection will stay open on the server side.
- client_header_timeout: 60s # timeout for reading client request header, then 408 (Request Time-out) error is returned to the client
- client_body_timeout: 60s # timeout for reading client request body, then 408 (Request Time-out) error is returned to the client
- send_timeout: 10s # timeout for transmitting a response to the client.then the connection is closed
- underscores_in_headers: "on" # default enables the use of underscores in client request header fields
- real_ip_header: "X-Real-IP" # http://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header
- real_ip_from: # http://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from
- - 127.0.0.1
- - 'unix:'
- #lua_shared_dicts: # add custom shared cache to nginx.conf
- # ipc_shared_dict: 100m # custom shared cache, format: `cache-key: cache-size`
etcd:
host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
- "http://192.17.5.10:2379" # multiple etcd address
prefix: "/apisix" # apisix configurations prefix
- timeout: 3 # 3 seconds
-
-plugins: # plugin list
- - example-plugin
- - limit-req
- - limit-count
- - limit-conn
- - key-auth
- - basic-auth
- - prometheus
- - node-status
- - jwt-auth
- - zipkin
- - ip-restriction
- - grpc-transcode
- - serverless-pre-function
- - serverless-post-function
- - openid-connect
- - proxy-rewrite
- - redirect
- - response-rewrite
- - fault-injection
- - udp-logger
- - wolf-rbac
- - proxy-cache
- - tcp-logger
- - proxy-mirror
- - kafka-logger
- - cors
-stream_plugins:
- - mqtt-proxy
+ timeout: 30 # 30 seconds
+
\ No newline at end of file
diff --git a/compose/dashboard_conf/nginx.conf b/compose/dashboard_conf/nginx.conf
new file mode 100644
index 0000000000..3735dd8de2
--- /dev/null
+++ b/compose/dashboard_conf/nginx.conf
@@ -0,0 +1,22 @@
+server {
+ listen 80;
+ # gzip config
+ gzip on;
+ gzip_min_length 1k;
+ gzip_comp_level 9;
+ gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
+ gzip_vary on;
+ gzip_disable "MSIE [1-6]\.";
+
+ root /usr/share/nginx/html;
+ include /etc/nginx/mime.types;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /apisix/admin {
+ proxy_pass http://manager:8080/apisix/admin;
+ }
+}
+
diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml
index c36495bd7c..78d391454b 100644
--- a/compose/docker-compose.yml
+++ b/compose/docker-compose.yml
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
version: "3"
services:
@@ -119,6 +135,11 @@ services:
restart: always
ports:
- "80:80/tcp"
+ volumes:
+ - "./dashboard_conf/nginx.conf:/etc/nginx/conf.d/default.conf"
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.18
networks:
apisix-dashboard:
diff --git a/compose/grafana_conf/provisioning/dashboards/all.yaml b/compose/grafana_conf/provisioning/dashboards/all.yaml
index c58cbc61f6..26a9599efe 100644
--- a/compose/grafana_conf/provisioning/dashboards/all.yaml
+++ b/compose/grafana_conf/provisioning/dashboards/all.yaml
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
apiVersion: 1
providers:
diff --git a/compose/grafana_conf/provisioning/datasources/all.yaml b/compose/grafana_conf/provisioning/datasources/all.yaml
index 4245eac0e4..3812d4f7b1 100644
--- a/compose/grafana_conf/provisioning/datasources/all.yaml
+++ b/compose/grafana_conf/provisioning/datasources/all.yaml
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
datasources:
- access: 'proxy'
editable: true
diff --git a/compose/manager_conf/build.sh b/compose/manager_conf/build.sh
index efedb1c9fa..22f2e2f47a 100755
--- a/compose/manager_conf/build.sh
+++ b/compose/manager_conf/build.sh
@@ -1,3 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
#!/bin/sh
pwd=`pwd`
@@ -16,5 +33,5 @@ sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
-cd /root/manager-api
+cd /go/manager-api
exec ./manager-api
diff --git a/compose/pics/grafana_1.png b/compose/pics/grafana_1.png
deleted file mode 100644
index 631276e151ff76396b767b292208d778da7d7a8b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 57816
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