diff --git a/.github/workflows/auto-build-rpm.yml b/.github/workflows/auto-build-rpm.yml
new file mode 100644
index 0000000000..7feaeb0480
--- /dev/null
+++ b/.github/workflows/auto-build-rpm.yml
@@ -0,0 +1,70 @@
+name: Auto Build RPM
+
+on:
+ push:
+ branches: [master, 'release/**']
+ paths-ignore:
+ - 'docs/**'
+ pull_request:
+ branches: [master]
+ paths-ignore:
+ - 'docs/**'
+
+jobs:
+ auto_build_rpm:
+ name: auto build rpm package
+ runs-on: ubuntu-latest
+
+ services:
+ etcd:
+ image: bitnami/etcd:3.4.0
+ ports:
+ - 2379:2379
+ - 2380:2380
+ env:
+ ALLOW_NONE_AUTHENTICATION: yes
+ ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v2
+
+ - name: Extract branch name
+ id: branch_env
+ shell: bash
+ run: |
+ echo "##[set-output name=version;]$(echo ${GITHUB_REF##*/})"
+
+ - name: Build rpm package
+ run: |
+ export VERSION=${{ steps.branch_env.outputs.version }}
+ sudo gem install --no-document fpm
+ git clone https://github.com/api7/apisix-build-tools.git
+ cd apisix-build-tools
+ export checkout=master
+ if [ "$VERSION" != "merge" && "$VERSION" != "master" ];then
+ export checkout=release/${VERSION}
+ fi
+ make package type=rpm app=dashboard version=${VERSION} checkout=${checkout}
+
+ - name: Run centos7 docker and mapping apisix into container
+ run: |
+ docker run -itd -v $PWD:/apisix-dashboard --name centos7Instance --net="host" docker.io/centos:7 /bin/bash
+
+ - name: Install rpm package
+ run: |
+ export VERSION=${{ steps.branch_env.outputs.version }}
+ docker exec centos7Instance bash -c "cd apisix-dashboard && yum install -y ./apisix-build-tools/output/apisix-dashboard-${VERSION}-0.x86_64.rpm"
+ docker logs centos7Instance
+ # Dependencies are attached with rpm, so revert `make deps`
+ docker exec centos7Instance bash -c "cd /usr/local/apisix/dashboard/ && nohup ./manager-api &"
+
+ - name: Run test cases
+ run: |
+ api/test/shell/manager_smoking.sh -s true
+
+ - name: Publish Artifact
+ uses: actions/upload-artifact@v2.2.2
+ with:
+ name: "rpm"
+ path: "./apisix-build-tools/output/apisix-dashboard-${{ steps.branch_env.outputs.version }}-0.x86_64.rpm"
diff --git a/.github/workflows/backend-e2e-test.yml b/.github/workflows/backend-e2e-test.yml
index 781b44e1d1..a7a3545da7 100644
--- a/.github/workflows/backend-e2e-test.yml
+++ b/.github/workflows/backend-e2e-test.yml
@@ -45,6 +45,7 @@ jobs:
docker-compose up -d
sleep 5
docker logs docker_managerapi_1
+ docker logs docker_apisix_1
- name: run test
working-directory: ./api/test/e2e
diff --git a/api/conf/schema.json b/api/conf/schema.json
index e69c57aa6e..ff58ff4b78 100644
--- a/api/conf/schema.json
+++ b/api/conf/schema.json
@@ -10,17 +10,6 @@
"maxLength": 256,
"type": "string"
},
- "id": {
- "anyOf": [{
- "maxLength": 64,
- "minLength": 1,
- "pattern": "^[a-zA-Z0-9-_.]+$",
- "type": "string"
- }, {
- "minimum": 1,
- "type": "integer"
- }]
- },
"labels": {
"description": "key/value pairs to specify attributes",
"maxProperties": 16,
@@ -147,6 +136,27 @@
"maxLength": 1048576,
"minLength": 1,
"type": "string"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "desc": {
+ "maxLength": 256,
+ "type": "string"
+ },
+ "id": {
+ "anyOf": [{
+ "maxLength": 64,
+ "minLength": 1,
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "type": "string"
+ }, {
+ "minimum": 1,
+ "type": "integer"
+ }]
+ },
+ "update_time": {
+ "type": "integer"
}
},
"required": ["content"],
@@ -595,10 +605,6 @@
"description": "discovery type",
"type": "string"
},
- "enable_websocket": {
- "description": "enable websocket for request",
- "type": "boolean"
- },
"hash_on": {
"default": "vars",
"enum": ["consumer", "cookie", "header", "vars", "vars_combinations"],
@@ -664,6 +670,11 @@
"minimum": 1,
"type": "integer"
},
+ "priority": {
+ "default": 0,
+ "description": "priority of node",
+ "type": "integer"
+ },
"weight": {
"description": "weight of node",
"minimum": 0,
@@ -754,12 +765,6 @@
"uniqueItems": true
},
"vars": {
- "items": {
- "description": "Nginx builtin variable name and value",
- "maxItems": 4,
- "minItems": 2,
- "type": "array"
- },
"type": "array"
}
},
@@ -1025,10 +1030,6 @@
"description": "discovery type",
"type": "string"
},
- "enable_websocket": {
- "description": "enable websocket for request",
- "type": "boolean"
- },
"hash_on": {
"default": "vars",
"enum": ["consumer", "cookie", "header", "vars", "vars_combinations"],
@@ -1094,6 +1095,11 @@
"minimum": 1,
"type": "integer"
},
+ "priority": {
+ "default": 0,
+ "description": "priority of node",
+ "type": "integer"
+ },
"weight": {
"description": "weight of node",
"minimum": 0,
@@ -1270,6 +1276,13 @@
},
"stream_route": {
"properties": {
+ "create_time": {
+ "type": "integer"
+ },
+ "desc": {
+ "maxLength": 256,
+ "type": "string"
+ },
"id": {
"anyOf": [{
"maxLength": 64,
@@ -1330,6 +1343,9 @@
"description": "server port",
"type": "integer"
},
+ "update_time": {
+ "type": "integer"
+ },
"upstream": {
"additionalProperties": false,
"oneOf": [{
@@ -1535,10 +1551,6 @@
"description": "discovery type",
"type": "string"
},
- "enable_websocket": {
- "description": "enable websocket for request",
- "type": "boolean"
- },
"hash_on": {
"default": "vars",
"enum": ["consumer", "cookie", "header", "vars", "vars_combinations"],
@@ -1604,6 +1616,11 @@
"minimum": 1,
"type": "integer"
},
+ "priority": {
+ "default": 0,
+ "description": "priority of node",
+ "type": "integer"
+ },
"weight": {
"description": "weight of node",
"minimum": 0,
@@ -1887,10 +1904,6 @@
"description": "discovery type",
"type": "string"
},
- "enable_websocket": {
- "description": "enable websocket for request",
- "type": "boolean"
- },
"hash_on": {
"default": "vars",
"enum": ["consumer", "cookie", "header", "vars", "vars_combinations"],
@@ -1956,6 +1969,11 @@
"minimum": 1,
"type": "integer"
},
+ "priority": {
+ "default": 0,
+ "description": "priority of node",
+ "type": "integer"
+ },
"weight": {
"description": "weight of node",
"minimum": 0,
@@ -2297,54 +2315,59 @@
"priority": 2400,
"schema": {
"$comment": "this is a mark for our injected plugin schema",
- "oneOf": [{
- "properties": {
- "blacklist": {
- "items": {
- "type": "string"
+ "anyOf": [{
+ "required": ["blacklist"]
+ }, {
+ "required": ["whitelist"]
+ }, {
+ "required": ["allowed_by_methods"]
+ }],
+ "properties": {
+ "allowed_by_methods": {
+ "items": {
+ "properties": {
+ "methods": {
+ "items": {
+ "enum": ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"],
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "user": {
+ "type": "string"
+ }
},
- "minItems": 1,
- "type": "array"
- },
- "rejected_code": {
- "default": 403,
- "minimum": 200,
- "type": "integer"
+ "type": "object"
},
- "type": {
- "default": "consumer_name",
- "enum": ["consumer_name", "service_id"],
- "type": "string"
- }
+ "type": "array"
},
- "required": ["blacklist"],
- "title": "blacklist"
- }, {
- "properties": {
- "rejected_code": {
- "default": 403,
- "minimum": 200,
- "type": "integer"
- },
- "type": {
- "default": "consumer_name",
- "enum": ["consumer_name", "service_id"],
+ "blacklist": {
+ "items": {
"type": "string"
},
- "whitelist": {
- "items": {
- "type": "string"
- },
- "minItems": 1,
- "type": "array"
- }
+ "minItems": 1,
+ "type": "array"
},
- "required": ["whitelist"],
- "title": "whitelist"
- }],
- "properties": {
"disable": {
"type": "boolean"
+ },
+ "rejected_code": {
+ "default": 403,
+ "minimum": 200,
+ "type": "integer"
+ },
+ "type": {
+ "default": "consumer_name",
+ "enum": ["consumer_name", "service_id"],
+ "type": "string"
+ },
+ "whitelist": {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
}
},
"type": "object"
@@ -2376,6 +2399,17 @@
"description": "you can use '*' to allow all origins when no credentials,'**' to allow forcefully(it will bring some security risks, be carefully),multiple origin use ',' to split. default: *.",
"type": "string"
},
+ "allow_origins_by_regex": {
+ "description": "you can use regex to allow specific origins when no credentials,for example use [.*\\.test.com] to allow a.test.com and b.test.com",
+ "items": {
+ "maxLength": 4096,
+ "minLength": 1,
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ },
"disable": {
"type": "boolean"
},
@@ -2608,6 +2642,9 @@
"type": "integer"
},
"vars": {
+ "items": {
+ "type": "array"
+ },
"maxItems": 20,
"type": "array"
}
@@ -2627,6 +2664,9 @@
"type": "integer"
},
"vars": {
+ "items": {
+ "type": "array"
+ },
"maxItems": 20,
"type": "array"
}
@@ -3682,6 +3722,9 @@
"maximum": 598,
"minimum": 200,
"type": "integer"
+ },
+ "vars": {
+ "type": "array"
}
},
"type": "object"
@@ -3762,6 +3805,9 @@
"default": "http://127.0.0.1:12800",
"type": "string"
},
+ "report_interval": {
+ "type": "integer"
+ },
"service_instance_name": {
"default": "APISIX Instance Name",
"description": "User Service Instance Name",
@@ -4028,43 +4074,6 @@
"items": {
"properties": {
"vars": {
- "items": {
- "additionalItems": {
- "anyOf": [{
- "type": "string"
- }, {
- "type": "number"
- }, {
- "type": "boolean"
- }, {
- "items": {
- "anyOf": [{
- "maxLength": 100,
- "minLength": 1,
- "type": "string"
- }, {
- "type": "number"
- }, {
- "type": "boolean"
- }]
- },
- "type": "array",
- "uniqueItems": true
- }]
- },
- "items": [{
- "maxLength": 100,
- "minLength": 1,
- "type": "string"
- }, {
- "maxLength": 2,
- "minLength": 1,
- "type": "string"
- }],
- "maxItems": 10,
- "minItems": 0,
- "type": "array"
- },
"type": "array"
}
},
@@ -4283,10 +4292,6 @@
"description": "discovery type",
"type": "string"
},
- "enable_websocket": {
- "description": "enable websocket for request",
- "type": "boolean"
- },
"hash_on": {
"default": "vars",
"enum": ["consumer", "cookie", "header", "vars", "vars_combinations"],
@@ -4352,6 +4357,11 @@
"minimum": 1,
"type": "integer"
},
+ "priority": {
+ "default": 0,
+ "description": "priority of node",
+ "type": "integer"
+ },
"weight": {
"description": "weight of node",
"minimum": 0,
@@ -4578,6 +4588,10 @@
"default": "APISIX",
"description": "service name for zipkin reporter",
"type": "string"
+ },
+ "span_version": {
+ "default": 2,
+ "enum": [1, 2]
}
},
"required": ["endpoint", "sample_ratio"],
diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go
index c42d31f9db..b81c7f4a6d 100644
--- a/api/internal/core/entity/entity.go
+++ b/api/internal/core/entity/entity.go
@@ -197,11 +197,12 @@ func (upstream *Upstream) Parse2NameResponse() (*UpstreamNameResponse, error) {
// swagger:model Consumer
type Consumer struct {
- BaseInfo
- Username string `json:"username"`
- Desc string `json:"desc,omitempty"`
- Plugins map[string]interface{} `json:"plugins,omitempty"`
- Labels map[string]string `json:"labels,omitempty"`
+ Username string `json:"username"`
+ Desc string `json:"desc,omitempty"`
+ Plugins map[string]interface{} `json:"plugins,omitempty"`
+ Labels map[string]string `json:"labels,omitempty"`
+ CreateTime int64 `json:"create_time,omitempty"`
+ UpdateTime int64 `json:"update_time,omitempty"`
}
// swagger:model SSL
diff --git a/api/internal/core/store/store_mock.go b/api/internal/core/store/store_mock.go
index 436ef6ba6c..c6801d69eb 100644
--- a/api/internal/core/store/store_mock.go
+++ b/api/internal/core/store/store_mock.go
@@ -18,6 +18,7 @@ package store
import (
"context"
+ "sort"
"github.com/stretchr/testify/mock"
)
@@ -44,6 +45,15 @@ func (m *MockInterface) List(_ context.Context, input ListInput) (*ListOutput, e
} else {
r0 = ret.Get(0).(*ListOutput)
}
+
+ if input.Less == nil {
+ input.Less = defLessFunc
+ }
+
+ sort.Slice(r0.Rows, func(i, j int) bool {
+ return input.Less(r0.Rows[i], r0.Rows[j])
+ })
+
r1 = ret.Error(1)
return r0, r1
diff --git a/api/internal/core/store/validate_test.go b/api/internal/core/store/validate_test.go
index 795fab3ec5..7b0b6f37fc 100644
--- a/api/internal/core/store/validate_test.go
+++ b/api/internal/core/store/validate_test.go
@@ -76,7 +76,6 @@ func TestAPISIXJsonSchemaValidator_Validate(t *testing.T) {
consumer := &entity.Consumer{}
reqBody := `{
- "id": "jack",
"username": "jack",
"plugins": {
"limit-count": {
@@ -93,26 +92,6 @@ func TestAPISIXJsonSchemaValidator_Validate(t *testing.T) {
err = validator.Validate(consumer)
assert.Nil(t, err)
- consumer2 := &entity.Consumer{}
- reqBody = `{
- "username": "jack",
- "plugins": {
- "limit-count": {
- "count": 2,
- "time_window": 60,
- "rejected_code": 503,
- "key": "remote_addr"
- }
- },
- "desc": "test description"
- }`
- err = json.Unmarshal([]byte(reqBody), consumer2)
- assert.Nil(t, err)
-
- err = validator.Validate(consumer2)
- assert.NotNil(t, err)
- assert.EqualError(t, err, "schema validate failed: id: Must validate at least one schema (anyOf)\nid: Invalid type. Expected: string, given: null")
-
//check nil obj
err = validator.Validate(nil)
assert.NotNil(t, err)
@@ -121,17 +100,16 @@ func TestAPISIXJsonSchemaValidator_Validate(t *testing.T) {
//plugin schema fail
consumer3 := &entity.Consumer{}
reqBody = `{
- "id": "jack",
- "username": "jack",
- "plugins": {
- "limit-count": {
- "time_window": 60,
- "rejected_code": 503,
- "key": "remote_addr"
- }
- },
- "desc": "test description"
- }`
+ "username": "jack",
+ "plugins": {
+ "limit-count": {
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ }
+ },
+ "desc": "test description"
+ }`
err = json.Unmarshal([]byte(reqBody), consumer3)
assert.Nil(t, err)
err = validator.Validate(consumer3)
@@ -456,7 +434,6 @@ func TestAPISIXSchemaValidator_Validate(t *testing.T) {
// normal config, should pass
reqBody := `{
- "id": "jack",
"username": "jack",
"plugins": {
"limit-count": {
diff --git a/api/internal/handler/consumer/consumer.go b/api/internal/handler/consumer/consumer.go
index 7efb238f97..0b169bbcbc 100644
--- a/api/internal/handler/consumer/consumer.go
+++ b/api/internal/handler/consumer/consumer.go
@@ -17,21 +17,18 @@
package consumer
import (
- "net/http"
"reflect"
"strings"
+ "time"
"github.com/gin-gonic/gin"
"github.com/shiningrush/droplet"
- "github.com/shiningrush/droplet/data"
"github.com/shiningrush/droplet/wrapper"
wgin "github.com/shiningrush/droplet/wrapper/gin"
"github.com/apisix/manager-api/internal/core/entity"
"github.com/apisix/manager-api/internal/core/store"
"github.com/apisix/manager-api/internal/handler"
- "github.com/apisix/manager-api/internal/utils"
- "github.com/apisix/manager-api/internal/utils/consts"
)
type Handler struct {
@@ -120,6 +117,17 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) {
}
return true
},
+ Less: func(i, j interface{}) bool {
+ iBase := i.(*entity.Consumer)
+ jBase := j.(*entity.Consumer)
+ if iBase.CreateTime != jBase.CreateTime {
+ return iBase.CreateTime < jBase.CreateTime
+ }
+ if iBase.UpdateTime != jBase.UpdateTime {
+ return iBase.UpdateTime < jBase.UpdateTime
+ }
+ return iBase.Username < jBase.Username
+ },
PageSize: input.PageSize,
PageNumber: input.PageNumber,
})
@@ -137,16 +145,21 @@ type SetInput struct {
func (h *Handler) Set(c droplet.Context) (interface{}, error) {
input := c.Input().(*SetInput)
- if input.ID != nil && utils.InterfaceToString(input.ID) != input.Username {
- return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
- consts.ErrIDUsername
- }
if input.Username != "" {
input.Consumer.Username = input.Username
}
- input.Consumer.ID = input.Consumer.Username
ensurePluginsDefValue(input.Plugins)
+ // Because the ID of consumer has been removed,
+ // `BaseInfo` is no longer embedded in consumer's struct,
+ // So we need to maintain create_time and update_time separately for consumer
+ savedConsumer, _ := h.consumerStore.Get(c.Context(), input.Consumer.Username)
+ input.Consumer.CreateTime = time.Now().Unix()
+ input.Consumer.UpdateTime = time.Now().Unix()
+ if savedConsumer != nil {
+ input.Consumer.CreateTime = savedConsumer.(*entity.Consumer).CreateTime
+ }
+
ret, err := h.consumerStore.Update(c.Context(), &input.Consumer, true)
if err != nil {
return handler.SpecCodeResponse(err), err
diff --git a/api/internal/handler/consumer/consumer_test.go b/api/internal/handler/consumer/consumer_test.go
index 0bc5eab36f..9a76bc98b3 100644
--- a/api/internal/handler/consumer/consumer_test.go
+++ b/api/internal/handler/consumer/consumer_test.go
@@ -112,8 +112,8 @@ func TestHandler_List(t *testing.T) {
},
wantRet: &store.ListOutput{
Rows: []interface{}{
- &entity.Consumer{Username: "testUser"},
&entity.Consumer{Username: "iam-testUser"},
+ &entity.Consumer{Username: "testUser"},
&entity.Consumer{Username: "testUser-is-me"},
},
TotalSize: 3,
@@ -195,9 +195,6 @@ func TestHandler_Create(t *testing.T) {
},
giveCtx: context.WithValue(context.Background(), "test", "value"),
giveRet: &entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
@@ -207,9 +204,6 @@ func TestHandler_Create(t *testing.T) {
},
wantInput: &SetInput{
Consumer: entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
@@ -219,9 +213,6 @@ func TestHandler_Create(t *testing.T) {
},
},
wantRet: &entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
@@ -249,9 +240,6 @@ func TestHandler_Create(t *testing.T) {
giveErr: fmt.Errorf("create failed"),
wantInput: &SetInput{
Consumer: entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
@@ -275,10 +263,12 @@ func TestHandler_Create(t *testing.T) {
mStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
methodCalled = true
assert.Equal(t, tc.giveCtx, args.Get(0))
- assert.Equal(t, &tc.wantInput.Consumer, args.Get(1))
assert.True(t, args.Bool(2))
}).Return(tc.giveRet, tc.giveErr)
+ mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
+ }).Return(nil, nil)
+
h := Handler{consumerStore: mStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
@@ -302,6 +292,7 @@ func TestHandler_Update(t *testing.T) {
wantInput *entity.Consumer
wantRet interface{}
wantCalled bool
+ getRet interface{}
}{
{
caseDesc: "normal",
@@ -317,20 +308,15 @@ func TestHandler_Update(t *testing.T) {
},
giveCtx: context.WithValue(context.Background(), "test", "value"),
giveRet: &entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
"exp": 500,
},
},
+ CreateTime: 1618648423,
},
wantInput: &entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
@@ -339,17 +325,20 @@ func TestHandler_Update(t *testing.T) {
},
},
wantRet: &entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
"exp": 500,
},
},
+ CreateTime: 1618648423,
},
wantCalled: true,
+ getRet: &entity.Consumer{
+ Username: "name",
+ CreateTime: 1618648423,
+ UpdateTime: 1618648423,
+ },
},
{
caseDesc: "store update failed",
@@ -366,9 +355,6 @@ func TestHandler_Update(t *testing.T) {
},
giveErr: fmt.Errorf("create failed"),
wantInput: &entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: "name",
- },
Username: "name",
Plugins: map[string]interface{}{
"jwt-auth": map[string]interface{}{
@@ -391,10 +377,12 @@ func TestHandler_Update(t *testing.T) {
mStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
methodCalled = true
assert.Equal(t, tc.giveCtx, args.Get(0))
- assert.Equal(t, tc.wantInput, args.Get(1))
assert.True(t, args.Bool(2))
}).Return(tc.giveRet, tc.giveErr)
+ mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
+ }).Return(tc.getRet, nil)
+
h := Handler{consumerStore: mStore}
ctx := droplet.NewContext()
ctx.SetInput(tc.giveInput)
@@ -403,6 +391,10 @@ func TestHandler_Update(t *testing.T) {
assert.Equal(t, tc.wantCalled, methodCalled)
assert.Equal(t, tc.wantRet, ret)
assert.Equal(t, tc.wantErr, err)
+ if err == nil {
+ assert.Equal(t, tc.getRet.(*entity.Consumer).CreateTime, ret.(*entity.Consumer).CreateTime)
+ assert.NotEqual(t, tc.getRet.(*entity.Consumer).UpdateTime, ret.(*entity.Consumer).UpdateTime)
+ }
})
}
}
diff --git a/api/internal/handler/label/label_test.go b/api/internal/handler/label/label_test.go
index 664288d190..d227b95211 100644
--- a/api/internal/handler/label/label_test.go
+++ b/api/internal/handler/label/label_test.go
@@ -167,10 +167,6 @@ func genUpstream(labels map[string]string) *entity.Upstream {
func genConsumer(labels map[string]string) *entity.Consumer {
r := entity.Consumer{
- BaseInfo: entity.BaseInfo{
- ID: rand.Int(),
- CreateTime: rand.Int63(),
- },
Username: "test",
Labels: labels,
}
diff --git a/api/test/docker/apisix_config.yaml b/api/test/docker/apisix_config.yaml
index 00fc8782ad..e56a74d16a 100644
--- a/api/test/docker/apisix_config.yaml
+++ b/api/test/docker/apisix_config.yaml
@@ -22,7 +22,7 @@ etcd:
- "http://172.16.238.10:2379"
- "http://172.16.238.11:2379"
- "http://172.16.238.12:2379"
- resync_delay: 0.1 # sync data from etcd quickly for e2e test
+ resync_delay: 0 # sync data from etcd quickly for e2e test
apisix:
id: "apisix-server1"
diff --git a/api/test/docker/apisix_config2.yaml b/api/test/docker/apisix_config2.yaml
index bd788988c9..5d23725d52 100644
--- a/api/test/docker/apisix_config2.yaml
+++ b/api/test/docker/apisix_config2.yaml
@@ -22,7 +22,7 @@ etcd:
- "http://172.16.238.10:2379"
- "http://172.16.238.11:2379"
- "http://172.16.238.12:2379"
- resync_delay: 0.1 # sync data from etcd quickly for e2e test
+ resync_delay: 0 # sync data from etcd quickly for e2e test
apisix:
id: "apisix-server2"
diff --git a/api/test/docker/docker-compose.yaml b/api/test/docker/docker-compose.yaml
index 16d6e62d99..2da09908a2 100644
--- a/api/test/docker/docker-compose.yaml
+++ b/api/test/docker/docker-compose.yaml
@@ -127,7 +127,7 @@ services:
apisix:
hostname: apisix_server1
- image: apache/apisix:2.4-alpine
+ image: apache/apisix:2.5-alpine
restart: always
volumes:
- ./apisix_config.yaml:/usr/local/apisix/conf/config.yaml:ro
@@ -148,7 +148,7 @@ services:
apisix2:
hostname: apisix_server2
- image: apache/apisix:2.4-alpine
+ image: apache/apisix:2.5-alpine
restart: always
volumes:
- ./apisix_config2.yaml:/usr/local/apisix/conf/config.yaml:ro
diff --git a/api/test/e2enew/schema/schema_test.go b/api/test/e2enew/schema/schema_test.go
index 4f14d003b3..e856cc278b 100644
--- a/api/test/e2enew/schema/schema_test.go
+++ b/api/test/e2enew/schema/schema_test.go
@@ -70,7 +70,7 @@ var _ = ginkgo.Describe("Schema Test", func() {
Path: "/apisix/admin/schemas/consumer",
Headers: map[string]string{"Authorization": base.GetToken()},
ExpectStatus: http.StatusOK,
- ExpectBody: `"properties":{"create_time":{"type":"integer"},"desc":{"maxLength":256,"type":"string"},"id":{"anyOf":[{"maxLength":64,"minLength":1,"pattern":"^[a-zA-Z0-9-_.]+$","type":"string"},{"minimum":1,"type":"integer"}]},"labels":{"description":"key/value pairs to specify attributes","maxProperties":16,"patternProperties":{".*":{"description":"value of label","maxLength":64,"minLength":1,"pattern":"^\\S+$","type":"string"}},"type":"object"},"plugins":{"type":"object"},"update_time":{"type":"integer"},"username":{"maxLength":32,"minLength":1,"pattern":"^[a-zA-Z0-9_]+$","type":"string"}}`,
+ ExpectBody: `"properties":{"create_time":{"type":"integer"},"desc":{"maxLength":256,"type":"string"},"labels":{"description":"key/value pairs to specify attributes","maxProperties":16,"patternProperties":{".*":{"description":"value of label","maxLength":64,"minLength":1,"pattern":"^\\S+$","type":"string"}},"type":"object"},"plugins":{"type":"object"},"update_time":{"type":"integer"},"username":{"maxLength":32,"minLength":1,"pattern":"^[a-zA-Z0-9_]+$","type":"string"}}`,
Sleep: base.SleepTime,
}),
table.Entry("get schema of non-existent resources", base.HttpTestCase{
diff --git a/web/cypress/fixtures/plugin-dataset.json b/web/cypress/fixtures/plugin-dataset.json
index b097ea8582..5f3739ad67 100644
--- a/web/cypress/fixtures/plugin-dataset.json
+++ b/web/cypress/fixtures/plugin-dataset.json
@@ -280,7 +280,7 @@
}
},
{
- "shouldValid": false,
+ "shouldValid": true,
"data": {
"whitelist": ["jack1"],
"blacklist": ["jack2"]
@@ -1265,7 +1265,7 @@
}
},
{
- "shouldValid": false,
+ "shouldValid": true,
"data": {
"rules": [
{
diff --git a/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js b/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js
index 3bf211114e..9afa31613c 100644
--- a/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js
+++ b/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js
@@ -25,7 +25,8 @@ context('Create and Delete Consumer', () => {
});
const selector = {
- max_age: "#max_age"
+ max_age: "#max_age",
+ allow_origins_by_regex: "#allow_origins_by_regex_0"
}
const data = {
@@ -44,7 +45,9 @@ context('Create and Delete Consumer', () => {
// config auth plugin
cy.contains(this.domSelector.pluginCard, 'key-auth').within(() => {
- cy.contains('Enable').click({ force: true });
+ cy.contains('Enable').click({
+ force: true
+ });
});
cy.focused(this.domSelector.drawer).should('exist');
cy.get(this.domSelector.disabledSwitcher).click().should('have.class', 'ant-switch-checked');
@@ -69,7 +72,7 @@ context('Create and Delete Consumer', () => {
cy.get(this.domSelector.drawer).should('be.visible');
cy.get(selector.max_age).clear();
- // config proxy-mirror form
+ // config cors form
cy.get(this.domSelector.drawer).within(() => {
cy.contains('Submit').click({
force: true,
@@ -79,6 +82,7 @@ context('Create and Delete Consumer', () => {
cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist');
cy.get(selector.max_age).type(data.time);
+ cy.get(selector.allow_origins_by_regex).type('.*.test.com');
cy.get(this.domSelector.drawer).within(() => {
cy.contains('Submit').click({
force: true,
diff --git a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
index cd3a2a4857..255740c233 100644
--- a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
+++ b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
@@ -41,7 +41,7 @@ context('Create Configure and Delete PluginTemplate', () => {
cy.contains('proxy-rewrite').should('not.exist');
cy.contains(this.domSelector.pluginCard, 'basic-auth').within(() => {
- cy.contains('Enable').click({
+ cy.get('button').click({
force: true,
});
});
diff --git a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
index d62cd87018..aeb5118bee 100644
--- a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
+++ b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
@@ -61,7 +61,7 @@ context('Can select service_id skip upstream in route', () => {
cy.get(this.domSelector.name).type(this.data.routeName);
cy.contains('Next').click();
cy.get(this.domSelector.upstreamSelector).click();
- cy.contains('None').should('not.exist');
+ cy.get('.ant-select-item-option-disabled > .ant-select-item-option-content').contains('None');
cy.contains('Previous').click();
cy.wait(500);
@@ -91,9 +91,12 @@ context('Can select service_id skip upstream in route', () => {
cy.contains(this.data.routeName).siblings().contains('Configure').click();
cy.get(this.domSelector.serviceSelector).click();
cy.contains('None').click();
+ cy.get(this.domSelector.notification).should('contain', 'Please check the configuration of binding service');
+ cy.get(this.domSelector.notificationCloseIcon).click();
+
cy.contains('Next').click();
- cy.get(this.domSelector.upstream_id).click();
- cy.contains('None').should('not.exist');
+ cy.wait(500);
+ cy.get('[data-cy=upstream_selector]').click();
cy.contains(this.data.upstreamName).click();
cy.contains('Next').click();
cy.contains('Next').click();
@@ -101,13 +104,7 @@ context('Can select service_id skip upstream in route', () => {
cy.contains(this.data.submitSuccess);
});
- it('should delete upstream, service and route', function () {
- cy.visit('/');
- cy.contains('Service').click();
- cy.contains(this.data.serviceName).siblings().contains('Delete').click();
- cy.contains('button', 'Confirm').click();
- cy.get(this.domSelector.notification).should('contain', this.data.deleteServiceSuccess);
-
+ it('should delete route, service and upstream', function () {
cy.visit('/');
cy.contains('Route').click();
cy.contains(this.data.routeName).siblings().contains('More').click();
@@ -115,7 +112,13 @@ context('Can select service_id skip upstream in route', () => {
cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
cy.contains('OK').click();
});
+
cy.get(this.domSelector.notification).should('contain', this.data.deleteRouteSuccess);
+ cy.visit('/');
+ cy.contains('Service').click();
+ cy.contains(this.data.serviceName).siblings().contains('Delete').click();
+ cy.contains('button', 'Confirm').click();
+ cy.get(this.domSelector.notification).should('contain', this.data.deleteServiceSuccess);
cy.visit('/');
cy.contains('Upstream').click();
diff --git a/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js b/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js
index f9db28bb56..b1df99e0e9 100644
--- a/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js
+++ b/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js
@@ -57,7 +57,7 @@ context('Create and delete route with limit-count form', () => {
// config limit-count form with local policy
cy.contains(this.domSelector.pluginCard, 'limit-count').within(() => {
- cy.contains('Enable').click({
+ cy.get('button').click({
force: true,
});
});
@@ -75,7 +75,7 @@ context('Create and delete route with limit-count form', () => {
// config limit-count form with redis policy
cy.contains(this.domSelector.pluginCard, 'limit-count').within(() => {
- cy.contains('Enable').click({
+ cy.get('button').click({
force: true,
});
});
@@ -97,7 +97,7 @@ context('Create and delete route with limit-count form', () => {
// config limit-count form with redis policy
cy.contains(this.domSelector.pluginCard, 'limit-count').within(() => {
- cy.contains('Enable').click({
+ cy.get('button').click({
force: true,
});
});
diff --git a/web/cypress/integration/route/create-route-with-upstream.spec.js b/web/cypress/integration/route/create-route-with-upstream.spec.js
index 2a976b8a73..8a64a053d9 100644
--- a/web/cypress/integration/route/create-route-with-upstream.spec.js
+++ b/web/cypress/integration/route/create-route-with-upstream.spec.js
@@ -54,8 +54,7 @@ context('Create Route with Upstream', () => {
cy.get(this.domSelector.input).should('be.disabled');
// should enable Upstream input boxes after selecting Custom mode
cy.get(this.domSelector.upstreamSelector).click();
- cy.contains('Custom').click();
- cy.get(this.domSelector.input).should('not.be.disabled');
+ cy.contains('.ant-select-item-option-content', 'Custom').click();
cy.get(this.domSelector.nodes_0_host).clear().type(this.data.ip1);
cy.get(this.domSelector.nodes_0_port).type(this.data.port);
@@ -77,7 +76,9 @@ context('Create Route with Upstream', () => {
cy.contains(this.data.routeName).siblings().contains('Configure').click();
cy.get(this.domSelector.name).should('value', this.data.routeName);
- cy.contains('Next').click({ force: true });
+ cy.contains('Next').click({
+ force: true
+ });
// check if the changes have been saved
cy.get(this.domSelector.nodes_0_host).should('value', this.data.ip1);
@@ -87,7 +88,7 @@ context('Create Route with Upstream', () => {
cy.get(this.domSelector.input).should('be.disabled');
cy.contains(this.data.upstreamName).click();
- cy.contains('Custom').click();
+ cy.contains('.ant-select-item-option-content', 'Custom').click();
cy.get(this.domSelector.input).should('not.be.disabled');
cy.get(this.domSelector.nodes_0_host).clear().type(this.data.ip2);
@@ -107,7 +108,9 @@ context('Create Route with Upstream', () => {
cy.contains(this.data.routeName).siblings().contains('Configure').click();
// ensure it has already changed to edit page
cy.get(this.domSelector.name).should('value', this.data.routeName);
- cy.contains('Next').click({ force: true });
+ cy.contains('Next').click({
+ force: true
+ });
cy.get(this.domSelector.nodes_0_host).should('value', this.data.ip2);
});
diff --git a/web/cypress/integration/service/edit-service-with-upstream.spec.js b/web/cypress/integration/service/edit-service-with-upstream.spec.js
index 87be509c63..150a9e4a57 100644
--- a/web/cypress/integration/service/edit-service-with-upstream.spec.js
+++ b/web/cypress/integration/service/edit-service-with-upstream.spec.js
@@ -40,6 +40,7 @@ context('Edit Service with Upstream', () => {
it('should create a test service', function () {
cy.visit('/');
+ cy.get('.ant-empty').should('be.visible');
cy.contains('Service').click();
cy.get(this.domSelector.empty).should('be.visible');
cy.contains('Create').click();
@@ -62,11 +63,14 @@ context('Edit Service with Upstream', () => {
cy.contains('Search').click();
cy.contains(this.data.serviceName).siblings().contains('Configure').click();
- cy.get(this.domSelector.nodes_0_host).click({ force: true }).should('value', this.data.ip1);
+ cy.wait(500);
+ cy.get(this.domSelector.nodes_0_host).click({
+ force: true
+ }).should('value', this.data.ip1);
cy.get(this.domSelector.input).should('be.disabled');
cy.get(this.domSelector.upstreamSelector).click();
- cy.contains('Custom').click();
+ cy.contains('.ant-select-item-option-content', 'Custom').click();
cy.get(this.domSelector.nodes_0_host).should('not.be.disabled').clear().type(this.data.ip2);
cy.get(this.domSelector.nodes_0_port).type(this.data.port);
cy.get(this.domSelector.nodes_0_weight).type(this.data.weight);
diff --git a/web/cypress/support/commands.js b/web/cypress/support/commands.js
index 590243cb5f..4592efbe30 100644
--- a/web/cypress/support/commands.js
+++ b/web/cypress/support/commands.js
@@ -59,7 +59,7 @@ Cypress.Commands.add('configurePlugins', (cases) => {
cy.contains(name)
.parents(domSelector.parents)
.within(() => {
- cy.contains('Enable').click({
+ cy.get('button').click({
force: true,
});
});
diff --git a/web/package.json b/web/package.json
index f138a1bd00..d0c4d13cf2 100644
--- a/web/package.json
+++ b/web/package.json
@@ -52,7 +52,6 @@
"@ant-design/icons": "^4.0.0",
"@ant-design/pro-layout": "^6.0.0",
"@ant-design/pro-table": "2.30.1",
- "@api7-dashboard/ui": "^1.0.3",
"@mrblenny/react-flow-chart": "^0.0.14",
"@rjsf/antd": "2.2.0",
"@rjsf/core": "2.2.0",
diff --git a/web/src/components/PanelSection/index.tsx b/web/src/components/PanelSection/index.tsx
new file mode 100644
index 0000000000..0fdfd10a92
--- /dev/null
+++ b/web/src/components/PanelSection/index.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+import React, { CSSProperties } from 'react';
+import { Divider, Tooltip } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+
+const PanelSection: React.FC<{
+ title: string;
+ style?: CSSProperties;
+ id?: string;
+ tooltip?: string;
+}> = ({ title, style, id, children, tooltip }) => {
+ return (
+
+
+ {title}
+
+ {tooltip && }
+
+
{children}
+
+ );
+};
+
+export default PanelSection;
diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx
index 385f9670eb..72c474b5e4 100644
--- a/web/src/components/Plugin/PluginDetail.tsx
+++ b/web/src/components/Plugin/PluginDetail.tsx
@@ -285,6 +285,7 @@ const PluginDetail: React.FC = ({
title={formatMessage({ id: 'page.plugin.drawer.popconfirm.title.delete' })}
okText={formatMessage({ id: 'component.global.confirm' })}
cancelText={formatMessage({ id: 'component.global.cancel' })}
+ disabled={readonly}
onConfirm={() => {
onChange({
formData: form.getFieldsValue(),
@@ -294,13 +295,14 @@ const PluginDetail: React.FC = ({
}}
>
{initialData[name] ? (
-