diff --git a/.github/workflows/backend-cli-test.yml b/.github/workflows/backend-cli-test.yml index 55b42bb2ad..23234d78f8 100644 --- a/.github/workflows/backend-cli-test.yml +++ b/.github/workflows/backend-cli-test.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - etcd: [3.4.13] + etcd: [3.4.14] services: etcd: image: bitnami/etcd:${{ matrix.etcd }} diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index 7f495aa5b6..635e22d427 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -12,25 +12,6 @@ on: - 'api/**' jobs: - go-filter: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodule: true - - - uses: ./.github/actions/paths-filter - id: changes - with: - filters: | - go: - - '**.go' - working-directory: 'api' - list-files: shell - outputs: - matches: ${{ steps.changes.outputs.go }} - files: ${{ steps.changes.outputs.go_files }} - golangci: runs-on: ubuntu-latest needs: go-filter @@ -45,11 +26,8 @@ jobs: working-directory: api args: --tests=false only-new-issues: true - gofmt: runs-on: ubuntu-latest - needs: go-filter - if: needs.go-filter.outputs.matches == 'true' steps: - uses: actions/checkout@v2 - name: setup go diff --git a/api/conf/schema.json b/api/conf/schema.json index 9b454b5bf7..2fbbeedd7b 100644 --- a/api/conf/schema.json +++ b/api/conf/schema.json @@ -1,7 +1,6 @@ { "main": { "consumer": { - "additionalProperties": false, "properties": { "create_time": { "type": "integer" @@ -41,7 +40,6 @@ "type": "object" }, "global_rule": { - "additionalProperties": false, "properties": { "create_time": { "type": "integer" @@ -68,7 +66,6 @@ "type": "object" }, "plugin_config": { - "additionalProperties": false, "properties": { "create_time": { "type": "integer" @@ -115,7 +112,6 @@ "plugins": { "items": { "properties": { - "additionalProperties": false, "name": { "minLength": 1, "type": "string" @@ -130,7 +126,6 @@ "type": "array" }, "proto": { - "additionalProperties": false, "properties": { "content": { "maxLength": 1048576, @@ -163,7 +158,6 @@ "type": "object" }, "route": { - "additionalProperties": false, "allOf": [{ "oneOf": [{ "required": ["uri"] @@ -419,7 +413,6 @@ "type": "integer" }, "upstream": { - "additionalProperties": false, "oneOf": [{ "required": ["nodes", "type"] }, { @@ -427,7 +420,6 @@ }], "properties": { "checks": { - "additionalProperties": false, "anyOf": [{ "required": ["active"] }, { @@ -652,6 +644,26 @@ "type": "integer" }] }, + "keepalive_pool": { + "properties": { + "idle_timeout": { + "default": 60, + "minimum": 0, + "type": "number" + }, + "requests": { + "default": 1000, + "minimum": 1, + "type": "integer" + }, + "size": { + "default": 320, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, "key": { "description": "the key of chash for dynamic load balancing", "type": "string" @@ -728,6 +740,10 @@ "minimum": 0, "type": "integer" }, + "retry_timeout": { + "minimum": 0, + "type": "number" + }, "scheme": { "default": "http", "enum": ["grpc", "grpcs", "http", "https"] @@ -773,7 +789,6 @@ }, "type": { "description": "algorithms of load balancing", - "enum": ["chash", "ewma", "least_conn", "roundrobin"], "type": "string" }, "update_time": { @@ -818,7 +833,6 @@ "type": "object" }, "service": { - "additionalProperties": false, "properties": { "create_time": { "type": "integer" @@ -873,7 +887,6 @@ "type": "integer" }, "upstream": { - "additionalProperties": false, "oneOf": [{ "required": ["nodes", "type"] }, { @@ -881,7 +894,6 @@ }], "properties": { "checks": { - "additionalProperties": false, "anyOf": [{ "required": ["active"] }, { @@ -1106,6 +1118,26 @@ "type": "integer" }] }, + "keepalive_pool": { + "properties": { + "idle_timeout": { + "default": 60, + "minimum": 0, + "type": "number" + }, + "requests": { + "default": 1000, + "minimum": 1, + "type": "integer" + }, + "size": { + "default": 320, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, "key": { "description": "the key of chash for dynamic load balancing", "type": "string" @@ -1182,6 +1214,10 @@ "minimum": 0, "type": "integer" }, + "retry_timeout": { + "minimum": 0, + "type": "number" + }, "scheme": { "default": "http", "enum": ["grpc", "grpcs", "http", "https"] @@ -1227,7 +1263,6 @@ }, "type": { "description": "algorithms of load balancing", - "enum": ["chash", "ewma", "least_conn", "roundrobin"], "type": "string" }, "update_time": { @@ -1255,7 +1290,6 @@ "type": "object" }, "ssl": { - "additionalProperties": false, "oneOf": [{ "required": ["cert", "key", "sni"] }, { @@ -1444,7 +1478,6 @@ "type": "integer" }, "upstream": { - "additionalProperties": false, "oneOf": [{ "required": ["nodes", "type"] }, { @@ -1452,7 +1485,6 @@ }], "properties": { "checks": { - "additionalProperties": false, "anyOf": [{ "required": ["active"] }, { @@ -1677,6 +1709,26 @@ "type": "integer" }] }, + "keepalive_pool": { + "properties": { + "idle_timeout": { + "default": 60, + "minimum": 0, + "type": "number" + }, + "requests": { + "default": 1000, + "minimum": 1, + "type": "integer" + }, + "size": { + "default": 320, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, "key": { "description": "the key of chash for dynamic load balancing", "type": "string" @@ -1753,6 +1805,10 @@ "minimum": 0, "type": "integer" }, + "retry_timeout": { + "minimum": 0, + "type": "number" + }, "scheme": { "default": "http", "enum": ["grpc", "grpcs", "http", "https"] @@ -1798,7 +1854,6 @@ }, "type": { "description": "algorithms of load balancing", - "enum": ["chash", "ewma", "least_conn", "roundrobin"], "type": "string" }, "update_time": { @@ -1826,7 +1881,6 @@ "type": "object" }, "upstream": { - "additionalProperties": false, "oneOf": [{ "required": ["nodes", "type"] }, { @@ -1834,7 +1888,6 @@ }], "properties": { "checks": { - "additionalProperties": false, "anyOf": [{ "required": ["active"] }, { @@ -2059,6 +2112,26 @@ "type": "integer" }] }, + "keepalive_pool": { + "properties": { + "idle_timeout": { + "default": 60, + "minimum": 0, + "type": "number" + }, + "requests": { + "default": 1000, + "minimum": 1, + "type": "integer" + }, + "size": { + "default": 320, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, "key": { "description": "the key of chash for dynamic load balancing", "type": "string" @@ -2135,6 +2208,10 @@ "minimum": 0, "type": "integer" }, + "retry_timeout": { + "minimum": 0, + "type": "number" + }, "scheme": { "default": "http", "enum": ["grpc", "grpcs", "http", "https"] @@ -2180,7 +2257,6 @@ }, "type": { "description": "algorithms of load balancing", - "enum": ["chash", "ewma", "least_conn", "roundrobin"], "type": "string" }, "update_time": { @@ -2277,6 +2353,51 @@ }, "version": 0.1 }, + "authz-casbin": { + "metadata_schema": { + "properties": { + "model": { + "type": "string" + }, + "policy": { + "type": "string" + } + }, + "required": ["model", "policy"], + "type": "object" + }, + "priority": 2560, + "schema": { + "$comment": "this is a mark for our injected plugin schema", + "oneOf": [{ + "required": ["model_path", "policy_path", "username"] + }, { + "required": ["model", "policy", "username"] + }], + "properties": { + "disable": { + "type": "boolean" + }, + "model": { + "type": "string" + }, + "model_path": { + "type": "string" + }, + "policy": { + "type": "string" + }, + "policy_path": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "version": 0.1 + }, "authz-keycloak": { "priority": 2000, "schema": { @@ -2373,6 +2494,7 @@ "type": "boolean" }, "permissions": { + "default": {}, "items": { "maxLength": 100, "minLength": 1, @@ -2412,7 +2534,6 @@ }, "basic-auth": { "consumer_schema": { - "additionalProperties": false, "properties": { "password": { "type": "string" @@ -2428,7 +2549,6 @@ "priority": 2520, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" @@ -2442,7 +2562,6 @@ }, "batch-requests": { "metadata_schema": { - "additionalProperties": false, "properties": { "max_body_size": { "default": 1048576, @@ -2456,7 +2575,6 @@ "priority": 4010, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" @@ -2532,7 +2650,7 @@ }, "type": { "default": "consumer_name", - "enum": ["consumer_name", "service_id"], + "enum": ["consumer_name", "route_id", "service_id"], "type": "string" }, "whitelist": { @@ -2570,6 +2688,7 @@ "allow_origins": { "default": "*", "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: *.", + "pattern": "^(\\*|\\*\\*|null|\\w+://[^,]+(,\\w+://[^,]+)*)$", "type": "string" }, "allow_origins_by_regex": { @@ -2631,7 +2750,6 @@ "priority": 412, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "anyOf": [{ "required": ["before_body"] }, { @@ -2746,7 +2864,6 @@ }, "example-plugin": { "metadata_schema": { - "additionalProperties": false, "properties": { "ikey": { "minimum": 0, @@ -2976,9 +3093,71 @@ }, "version": 0.1 }, + "gzip": { + "priority": 995, + "schema": { + "$comment": "this is a mark for our injected plugin schema", + "properties": { + "buffers": { + "default": { + "number": 32, + "size": 4096 + }, + "properties": { + "number": { + "default": 32, + "minimum": 1, + "type": "integer" + }, + "size": { + "default": 4096, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "comp_level": { + "default": 1, + "maximum": 9, + "minimum": 1, + "type": "integer" + }, + "disable": { + "type": "boolean" + }, + "http_version": { + "default": 1.1, + "enum": [1, 1.1] + }, + "min_length": { + "default": 20, + "minimum": 1, + "type": "integer" + }, + "types": { + "anyOf": [{ + "items": { + "minLength": 1, + "type": "string" + }, + "minItem": 1, + "type": "array" + }, { + "enum": ["*"] + }], + "default": ["text/html"] + }, + "vary": { + "type": "boolean" + } + }, + "type": "object" + }, + "version": 0.1 + }, "hmac-auth": { "consumer_schema": { - "additionalProperties": false, "properties": { "access_key": { "maxLength": 256, @@ -3025,7 +3204,6 @@ "priority": 2530, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" @@ -3039,7 +3217,6 @@ }, "http-logger": { "metadata_schema": { - "additionalProperties": false, "properties": { "log_format": { "default": { @@ -3121,67 +3298,65 @@ "schema": { "$comment": "this is a mark for our injected plugin schema", "oneOf": [{ - "additionalProperties": false, - "properties": { - "whitelist": { - "items": { - "anyOf": [{ - "format": "ipv4", - "title": "IPv4", - "type": "string" - }, { - "pattern": "^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([12]?[0-9]|3[0-2])$", - "title": "IPv4/CIDR", - "type": "string" - }, { - "format": "ipv6", - "title": "IPv6", - "type": "string" - }, { - "pattern": "^([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?/[0-9]{1,3}$", - "title": "IPv6/CIDR", - "type": "string" - }] - }, - "minItems": 1, - "type": "array" - } - }, - "required": ["whitelist"], - "title": "whitelist" + "required": ["whitelist"] }, { - "additionalProperties": false, - "properties": { - "blacklist": { - "items": { - "anyOf": [{ - "format": "ipv4", - "title": "IPv4", - "type": "string" - }, { - "pattern": "^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([12]?[0-9]|3[0-2])$", - "title": "IPv4/CIDR", - "type": "string" - }, { - "format": "ipv6", - "title": "IPv6", - "type": "string" - }, { - "pattern": "^([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?/[0-9]{1,3}$", - "title": "IPv6/CIDR", - "type": "string" - }] - }, - "minItems": 1, - "type": "array" - } - }, - "required": ["blacklist"], - "title": "blacklist" + "required": ["blacklist"] }], "properties": { + "blacklist": { + "items": { + "anyOf": [{ + "format": "ipv4", + "title": "IPv4", + "type": "string" + }, { + "pattern": "^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([12]?[0-9]|3[0-2])$", + "title": "IPv4/CIDR", + "type": "string" + }, { + "format": "ipv6", + "title": "IPv6", + "type": "string" + }, { + "pattern": "^([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?/[0-9]{1,3}$", + "title": "IPv6/CIDR", + "type": "string" + }] + }, + "minItems": 1, + "type": "array" + }, "disable": { "type": "boolean" + }, + "message": { + "default": "Your IP address is not allowed", + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "whitelist": { + "items": { + "anyOf": [{ + "format": "ipv4", + "title": "IPv4", + "type": "string" + }, { + "pattern": "^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([12]?[0-9]|3[0-2])$", + "title": "IPv4/CIDR", + "type": "string" + }, { + "format": "ipv6", + "title": "IPv6", + "type": "string" + }, { + "pattern": "^([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?/[0-9]{1,3}$", + "title": "IPv6/CIDR", + "type": "string" + }] + }, + "minItems": 1, + "type": "array" } }, "type": "object" @@ -3243,7 +3418,6 @@ "priority": 2510, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" @@ -3255,6 +3429,19 @@ "version": 0.1 }, "kafka-logger": { + "metadata_schema": { + "properties": { + "log_format": { + "default": { + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr", + "host": "$host" + }, + "type": "object" + } + }, + "type": "object" + }, "priority": 403, "schema": { "$comment": "this is a mark for our injected plugin schema", @@ -3327,7 +3514,6 @@ }, "key-auth": { "consumer_schema": { - "additionalProperties": false, "properties": { "key": { "type": "string" @@ -3339,7 +3525,6 @@ "priority": 2500, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" @@ -3347,6 +3532,10 @@ "header": { "default": "apikey", "type": "string" + }, + "query": { + "default": "apikey", + "type": "string" } }, "type": "object" @@ -3375,14 +3564,12 @@ "type": "boolean" }, "key": { - "enum": ["consumer_name", "http_x_forwarded_for", "http_x_real_ip", "remote_addr", "server_addr"], + "enum": ["remote_addr", "server_addr"], "type": "string" }, - "rejected_code": { - "default": 503, - "maximum": 599, - "minimum": 200, - "type": "integer" + "only_use_default_delay": { + "default": false, + "type": "boolean" } }, "required": ["burst", "conn", "default_conn_delay", "key"], @@ -3464,6 +3651,10 @@ } }, "properties": { + "allow_degradation": { + "default": false, + "type": "boolean" + }, "count": { "exclusiveMinimum": 0, "type": "integer" @@ -3487,6 +3678,14 @@ "minimum": 200, "type": "integer" }, + "rejected_msg": { + "minLength": 1, + "type": "string" + }, + "show_limit_quota_header": { + "default": true, + "type": "boolean" + }, "time_window": { "exclusiveMinimum": 0, "type": "integer" @@ -3502,6 +3701,10 @@ "schema": { "$comment": "this is a mark for our injected plugin schema", "properties": { + "allow_degradation": { + "default": false, + "type": "boolean" + }, "burst": { "minimum": 0, "type": "number" @@ -3526,6 +3729,10 @@ "maximum": 599, "minimum": 200, "type": "integer" + }, + "rejected_msg": { + "minLength": 1, + "type": "string" } }, "required": ["burst", "key", "rate"], @@ -3690,7 +3897,6 @@ "priority": 500, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" @@ -3742,8 +3948,8 @@ "cache_method": { "default": ["GET", "HEAD"], "items": { - "description": "HTTP method", - "enum": ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"], + "description": "supported http method", + "enum": ["GET", "HEAD", "POST"], "type": "string" }, "minItems": 1, @@ -3799,7 +4005,6 @@ "priority": 1008, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "minProperties": 1, "properties": { "disable": { @@ -3842,6 +4047,47 @@ }, "version": 0.1 }, + "real-ip": { + "priority": 23000, + "schema": { + "$comment": "this is a mark for our injected plugin schema", + "properties": { + "disable": { + "type": "boolean" + }, + "source": { + "minLength": 1, + "type": "string" + }, + "trusted_addresses": { + "items": { + "anyOf": [{ + "format": "ipv4", + "title": "IPv4", + "type": "string" + }, { + "pattern": "^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([12]?[0-9]|3[0-2])$", + "title": "IPv4/CIDR", + "type": "string" + }, { + "format": "ipv6", + "title": "IPv6", + "type": "string" + }, { + "pattern": "^([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?/[0-9]{1,3}$", + "title": "IPv6/CIDR", + "type": "string" + }] + }, + "minItems": 1, + "type": "array" + } + }, + "required": ["source"], + "type": "object" + }, + "version": 0.1 + }, "redirect": { "priority": 900, "schema": { @@ -3897,7 +4143,6 @@ "priority": 2990, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "bypass_missing": { "default": false, @@ -3925,6 +4170,11 @@ "schema": { "$comment": "this is a mark for our injected plugin schema", "properties": { + "algorithm": { + "default": "uuid", + "enum": ["snowflake", "uuid"], + "type": "string" + }, "disable": { "type": "boolean" }, @@ -3976,7 +4226,6 @@ "priority": 899, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "minProperties": 1, "properties": { "body": { @@ -4014,7 +4263,6 @@ "priority": 990, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" @@ -4335,14 +4583,12 @@ "priority": 966, "schema": { "$comment": "this is a mark for our injected plugin schema", - "additionalProperties": false, "properties": { "disable": { "type": "boolean" }, "rules": { "items": { - "additionalProperties": false, "properties": { "match": { "items": { @@ -4362,7 +4608,6 @@ "items": { "properties": { "upstream": { - "additionalProperties": false, "oneOf": [{ "required": ["nodes", "type"] }, { @@ -4370,7 +4615,6 @@ }], "properties": { "checks": { - "additionalProperties": false, "anyOf": [{ "required": ["active"] }, { @@ -4595,6 +4839,26 @@ "type": "integer" }] }, + "keepalive_pool": { + "properties": { + "idle_timeout": { + "default": 60, + "minimum": 0, + "type": "number" + }, + "requests": { + "default": 1000, + "minimum": 1, + "type": "integer" + }, + "size": { + "default": 320, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, "key": { "description": "the key of chash for dynamic load balancing", "type": "string" @@ -4671,6 +4935,10 @@ "minimum": 0, "type": "integer" }, + "retry_timeout": { + "minimum": 0, + "type": "number" + }, "scheme": { "default": "http", "enum": ["grpc", "grpcs", "http", "https"] @@ -4716,7 +4984,6 @@ }, "type": { "description": "algorithms of load balancing", - "enum": ["chash", "ewma", "least_conn", "roundrobin"], "type": "string" }, "update_time": { @@ -4763,6 +5030,37 @@ }, "version": 0.1 }, + "ua-restriction": { + "priority": 2999, + "schema": { + "$comment": "this is a mark for our injected plugin schema", + "properties": { + "allowlist": { + "minItems": 1, + "type": "array" + }, + "bypass_missing": { + "default": false, + "type": "boolean" + }, + "denylist": { + "minItems": 1, + "type": "array" + }, + "disable": { + "type": "boolean" + }, + "message": { + "default": "Not allowed", + "maxLength": 1024, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "version": 0.1 + }, "udp-logger": { "priority": 400, "schema": { @@ -4833,6 +5131,10 @@ "default": 403, "minimum": 200, "type": "integer" + }, + "rejected_msg": { + "minLength": 1, + "type": "string" } }, "required": ["block_rules"], diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index efa10beb32..1d17c9a4ff 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -97,10 +97,11 @@ type Route struct { } // --- structures for upstream start --- +type TimeoutValue float32 type Timeout struct { - Connect int `json:"connect,omitempty"` - Send int `json:"send,omitempty"` - Read int `json:"read,omitempty"` + Connect TimeoutValue `json:"connect,omitempty"` + Send TimeoutValue `json:"send,omitempty"` + Read TimeoutValue `json:"read,omitempty"` } type Node struct { @@ -133,16 +134,16 @@ type UnHealthy struct { } type Active struct { - Type string `json:"type,omitempty"` - Timeout int `json:"timeout,omitempty"` - Concurrency int `json:"concurrency,omitempty"` - Host string `json:"host,omitempty"` - Port int `json:"port,omitempty"` - HTTPPath string `json:"http_path,omitempty"` - HTTPSVerifyCertificate string `json:"https_verify_certificate,omitempty"` - Healthy Healthy `json:"healthy,omitempty"` - UnHealthy UnHealthy `json:"unhealthy,omitempty"` - ReqHeaders []string `json:"req_headers,omitempty"` + Type string `json:"type,omitempty"` + Timeout TimeoutValue `json:"timeout,omitempty"` + Concurrency int `json:"concurrency,omitempty"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + HTTPPath string `json:"http_path,omitempty"` + HTTPSVerifyCertificate string `json:"https_verify_certificate,omitempty"` + Healthy Healthy `json:"healthy,omitempty"` + UnHealthy UnHealthy `json:"unhealthy,omitempty"` + ReqHeaders []string `json:"req_headers,omitempty"` } type Passive struct { @@ -161,24 +162,32 @@ type UpstreamTLS struct { ClientKey string `json:"client_key,omitempty"` } +type UpstreamKeepalivePool struct { + IdleTimeout TimeoutValue `json:"idle_timeout,omitempty"` + Requests int `json:"requests,omitempty"` + Size int `json:"size"` +} + type UpstreamDef struct { - Nodes interface{} `json:"nodes,omitempty"` - Retries int `json:"retries,omitempty"` - Timeout interface{} `json:"timeout,omitempty"` - Type string `json:"type,omitempty"` - Checks interface{} `json:"checks,omitempty"` - HashOn string `json:"hash_on,omitempty"` - Key string `json:"key,omitempty"` - Scheme string `json:"scheme,omitempty"` - DiscoveryType string `json:"discovery_type,omitempty"` - DiscoveryArgs map[string]string `json:"discovery_args,omitempty"` - PassHost string `json:"pass_host,omitempty"` - UpstreamHost string `json:"upstream_host,omitempty"` - Name string `json:"name,omitempty"` - Desc string `json:"desc,omitempty"` - ServiceName string `json:"service_name,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - TLS *UpstreamTLS `json:"tls,omitempty"` + Nodes interface{} `json:"nodes,omitempty"` + Retries int `json:"retries,omitempty"` + Timeout *Timeout `json:"timeout,omitempty"` + Type string `json:"type,omitempty"` + Checks interface{} `json:"checks,omitempty"` + HashOn string `json:"hash_on,omitempty"` + Key string `json:"key,omitempty"` + Scheme string `json:"scheme,omitempty"` + DiscoveryType string `json:"discovery_type,omitempty"` + DiscoveryArgs map[string]string `json:"discovery_args,omitempty"` + PassHost string `json:"pass_host,omitempty"` + UpstreamHost string `json:"upstream_host,omitempty"` + Name string `json:"name,omitempty"` + Desc string `json:"desc,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + TLS *UpstreamTLS `json:"tls,omitempty"` + KeepalivePool *UpstreamKeepalivePool `json:"keepalive_pool,omitempty"` + RetryTimeout TimeoutValue `json:"retry_timeout,omitempty"` } // swagger:model Upstream diff --git a/api/internal/core/store/validate_test.go b/api/internal/core/store/validate_test.go index 7b0b6f37fc..805049975a 100644 --- a/api/internal/core/store/validate_test.go +++ b/api/internal/core/store/validate_test.go @@ -289,7 +289,7 @@ func TestAPISIXJsonSchemaValidator_Plugin(t *testing.T) { err = json.Unmarshal([]byte(reqBody), route) assert.Nil(t, err) err = validator.Validate(route) - assert.Equal(t, fmt.Errorf("schema validate failed: (root): Must validate one and only one schema (oneOf)\n(root): Additional property disable is not allowed\ndisable: Invalid type. Expected: boolean, given: integer"), err) + assert.Equal(t, fmt.Errorf("schema validate failed: disable: Invalid type. Expected: boolean, given: integer"), err) } func TestAPISIXJsonSchemaValidator_Route_checkRemoteAddr(t *testing.T) { @@ -447,23 +447,4 @@ func TestAPISIXSchemaValidator_Validate(t *testing.T) { }` err = validator.Validate([]byte(reqBody)) assert.Nil(t, err) - - // config with non existent field, should be failed. - reqBody = `{ - "username": "jack", - "not-exist": "val", - "plugins": { - "limit-count": { - "count": 2, - "time_window": 60, - "rejected_code": 503, - "key": "remote_addr" - } - }, - "desc": "test description" - }` - err = validator.Validate([]byte(reqBody)) - assert.NotNil(t, err) - assert.EqualError(t, err, "schema validate failed: (root): Additional property not-exist is not allowed") - } diff --git a/api/internal/handler/schema/plugin_test.go b/api/internal/handler/schema/plugin_test.go index d569f2d5d2..eb61e79394 100644 --- a/api/internal/handler/schema/plugin_test.go +++ b/api/internal/handler/schema/plugin_test.go @@ -62,5 +62,5 @@ func TestPlugin(t *testing.T) { // plugin type assert.ElementsMatch(t, []string{"basic-auth", "jwt-auth", "hmac-auth", "key-auth", "wolf-rbac"}, authPlugins) // consumer schema - assert.Equal(t, `{"additionalProperties":false,"properties":{"password":{"type":"string"},"username":{"type":"string"}},"required":["password","username"],"title":"work with consumer object","type":"object"}`, basicAuthConsumerSchema) + assert.Equal(t, `{"properties":{"password":{"type":"string"},"username":{"type":"string"}},"required":["password","username"],"title":"work with consumer object","type":"object"}`, basicAuthConsumerSchema) } diff --git a/api/internal/handler/upstream/upstream_test.go b/api/internal/handler/upstream/upstream_test.go index c2b274d4c8..47c219a4e5 100644 --- a/api/internal/handler/upstream/upstream_test.go +++ b/api/internal/handler/upstream/upstream_test.go @@ -55,10 +55,10 @@ func TestUpstream_Get(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -103,10 +103,10 @@ func TestUpstream_Get(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -345,10 +345,10 @@ func TestUpstream_Create(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -393,10 +393,10 @@ func TestUpstream_Create(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -441,10 +441,10 @@ func TestUpstream_Create(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -489,10 +489,10 @@ func TestUpstream_Create(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -542,10 +542,10 @@ func TestUpstream_Create(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -591,10 +591,10 @@ func TestUpstream_Create(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -689,10 +689,10 @@ func TestUpstream_Update(t *testing.T) { Upstream: entity.Upstream{ UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -738,10 +738,10 @@ func TestUpstream_Update(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -786,10 +786,10 @@ func TestUpstream_Update(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -834,10 +834,10 @@ func TestUpstream_Update(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -887,10 +887,10 @@ func TestUpstream_Update(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -973,10 +973,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": 15, - "send": 15, - "read": 15, + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -1022,10 +1022,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream2", - Timeout: map[string]interface{}{ - "connect": float64(20), - "send": float64(20), - "read": float64(20), + Timeout: &entity.Timeout{ + Connect: 20, + Send: 20, + Read: 20, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -1085,10 +1085,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream2", - Timeout: map[string]interface{}{ - "connect": float64(20), - "send": float64(20), - "read": float64(20), + Timeout: &entity.Timeout{ + Connect: 20, + Send: 20, + Read: 20, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -1138,10 +1138,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream2", - Timeout: map[string]interface{}{ - "connect": float64(20), - "send": float64(20), - "read": float64(20), + Timeout: &entity.Timeout{ + Connect: 20, + Send: 20, + Read: 20, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -1186,10 +1186,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream2", - Timeout: map[string]interface{}{ - "connect": float64(20), - "send": float64(20), - "read": float64(20), + Timeout: &entity.Timeout{ + Connect: 20, + Send: 20, + Read: 20, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -1243,10 +1243,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": float64(20), - "send": float64(20), - "read": float64(20), + Timeout: &entity.Timeout{ + Connect: 20, + Send: 20, + Read: 20, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -1291,10 +1291,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": float64(15), - "send": float64(15), - "read": float64(15), + Timeout: &entity.Timeout{ + Connect: 15, + Send: 15, + Read: 15, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ @@ -1339,10 +1339,10 @@ func TestUpstream_Patch(t *testing.T) { }, UpstreamDef: entity.UpstreamDef{ Name: "upstream1", - Timeout: map[string]interface{}{ - "connect": float64(20), - "send": float64(20), - "read": float64(20), + Timeout: &entity.Timeout{ + Connect: 20, + Send: 20, + Read: 20, }, Checks: map[string]interface{}{ "active": map[string]interface{}{ diff --git a/api/test/docker/docker-compose.yaml b/api/test/docker/docker-compose.yaml index 3f85c51cf6..072f312045 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.7-alpine + image: apache/apisix:2.9-alpine restart: always volumes: - ./apisix_config.yaml:/usr/local/apisix/conf/config.yaml:ro diff --git a/api/test/e2e/json_schema_validate_test.go b/api/test/e2e/json_schema_validate_test.go deleted file mode 100644 index df467426b1..0000000000 --- a/api/test/e2e/json_schema_validate_test.go +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 e2e - -import ( - "net/http" - "testing" -) - -func TestSchema_not_exist_field(t *testing.T) { - tests := []HttpTestCase{ - { - Desc: "config route with non-existent fields", - Object: ManagerApiExpect(t), - Path: "/apisix/admin/routes/r1", - Method: http.MethodPut, - Body: `{ - "name": "route1", - "uri": "/hello", - "nonexistent": "test non-existent", - "upstream": { - "type": "roundrobin", - "nodes": [{ - "host": "` + UpstreamIp + `", - "port": 1980, - "weight": 1 - }] - } - }`, - Headers: map[string]string{"Authorization": token}, - ExpectStatus: http.StatusBadRequest, - ExpectBody: `{"code":10000,"message":"schema validate failed: (root): Additional property nonexistent is not allowed"}`, - }, - { - Desc: "make sure the route create failed", - Object: APISIXExpect(t), - Method: http.MethodGet, - Path: "/hello", - ExpectStatus: http.StatusNotFound, - Sleep: sleepTime, - }, - } - - for _, tc := range tests { - testCaseCheck(tc, t) - } -} diff --git a/api/test/e2e/route_import_test.go b/api/test/e2e/route_import_test.go index bb70562ee5..b482688a07 100644 --- a/api/test/e2e/route_import_test.go +++ b/api/test/e2e/route_import_test.go @@ -319,7 +319,7 @@ func TestImport_with_multi_routes(t *testing.T) { ExpectBody: []string{`"methods":["GET","POST","HEAD","PUT","PATCH","DELETE"]`, `"proxy-rewrite":{"disable":false,"scheme":"https"}`, `"labels":{"API_VERSION":"v2","dev":"test"}`, - `"upstream":{"nodes":[{"host":"httpbin.org","port":443,"weight":1}],"timeout":{"connect":6000,"read":6000,"send":6000},"type":"roundrobin","pass_host":"node"}`, + `"upstream":{"nodes":[{"host":"httpbin.org","port":443,"weight":1}],"timeout":{"connect":6000,"send":6000,"read":6000},"type":"roundrobin","pass_host":"node"}`, }, Sleep: sleepTime, } @@ -335,7 +335,7 @@ func TestImport_with_multi_routes(t *testing.T) { ExpectBody: []string{`"methods":["POST"]`, `"proxy-rewrite":{"disable":false,"scheme":"https"}`, `"labels":{"API_VERSION":"v1","version":"v1"}`, - `"upstream":{"nodes":[{"host":"httpbin.org","port":443,"weight":1}],"timeout":{"connect":6000,"read":6000,"send":6000},"type":"roundrobin","pass_host":"node"}`, + `"upstream":{"nodes":[{"host":"httpbin.org","port":443,"weight":1}],"timeout":{"connect":6000,"send":6000,"read":6000},"type":"roundrobin","pass_host":"node"}`, }, Sleep: sleepTime, } diff --git a/api/test/e2enew/schema/schema_test.go b/api/test/e2enew/schema/schema_test.go index 50a55d3881..febb3c7245 100644 --- a/api/test/e2enew/schema/schema_test.go +++ b/api/test/e2enew/schema/schema_test.go @@ -52,7 +52,7 @@ var _ = ginkgo.Describe("Schema Test", func() { Path: "/apisix/admin/schema/plugins/jwt-auth", Headers: map[string]string{"Authorization": base.GetToken()}, ExpectStatus: http.StatusOK, - ExpectBody: "{\"$comment\":\"this is a mark for our injected plugin schema\",\"additionalProperties\":false,\"properties\":{\"disable\":{\"type\":\"boolean\"}},\"type\":\"object\"}", + ExpectBody: "{\"$comment\":\"this is a mark for our injected plugin schema\",\"properties\":{\"disable\":{\"type\":\"boolean\"}},\"type\":\"object\"}", Sleep: base.SleepTime, }), table.Entry("get schema of non-existent plugin", base.HttpTestCase{ diff --git a/api/test/e2enew/upstream/upstream_keepalive_pool.go b/api/test/e2enew/upstream/upstream_keepalive_pool.go new file mode 100644 index 0000000000..9047ac3899 --- /dev/null +++ b/api/test/e2enew/upstream/upstream_keepalive_pool.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 upstream + +import ( + "encoding/json" + "net/http" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + + "github.com/apisix/manager-api/test/e2enew/base" +) + +// just test for schema check +var _ = ginkgo.Describe("Upstream keepalive pool", func() { + ginkgo.It("create upstream with keepalive pool", func() { + createUpstreamBody := make(map[string]interface{}) + createUpstreamBody["nodes"] = []map[string]interface{}{ + { + "host": base.UpstreamIp, + "port": 1980, + "weight": 1, + }, + } + createUpstreamBody["type"] = "roundrobin" + createUpstreamBody["keepalive_pool"] = map[string]interface{}{ + "size": 320, + "requests": 1000, + "idle_timeout": 60, + } + _createUpstreamBody, err := json.Marshal(createUpstreamBody) + gomega.Expect(err).To(gomega.BeNil()) + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodPut, + Path: "/apisix/admin/upstreams/kp", + Body: string(_createUpstreamBody), + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("delete upstream", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodDelete, + Path: "/apisix/admin/upstreams/kp", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) +}) diff --git a/api/test/e2enew/upstream/upstream_retry.go b/api/test/e2enew/upstream/upstream_retry.go new file mode 100644 index 0000000000..adb81bb4f4 --- /dev/null +++ b/api/test/e2enew/upstream/upstream_retry.go @@ -0,0 +1,63 @@ +/* + * 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 upstream + +import ( + "encoding/json" + "net/http" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + + "github.com/apisix/manager-api/test/e2enew/base" +) + +// just test for schema check +var _ = ginkgo.Describe("Upstream keepalive pool", func() { + ginkgo.It("create upstream with keepalive pool", func() { + createUpstreamBody := make(map[string]interface{}) + createUpstreamBody["nodes"] = []map[string]interface{}{ + { + "host": base.UpstreamIp, + "port": 1980, + "weight": 1, + }, + } + createUpstreamBody["type"] = "roundrobin" + createUpstreamBody["retries"] = 5 + createUpstreamBody["retry_timeout"] = 5.5 + _createUpstreamBody, err := json.Marshal(createUpstreamBody) + gomega.Expect(err).To(gomega.BeNil()) + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodPut, + Path: "/apisix/admin/upstreams/retry", + Body: string(_createUpstreamBody), + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("delete upstream", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodDelete, + Path: "/apisix/admin/upstreams/retry", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) +}) diff --git a/api/test/e2enew/upstream/upstream_test.go b/api/test/e2enew/upstream/upstream_test.go index f0ff9383a4..2689b0a679 100644 --- a/api/test/e2enew/upstream/upstream_test.go +++ b/api/test/e2enew/upstream/upstream_test.go @@ -845,7 +845,6 @@ var _ = ginkgo.Describe("test upstream delete (route is in use)", func() { Body: `{ "name": "route1", "id": "r1", - "name": "route1", "uri": "/hello", "upstream_id": "u1" }`, diff --git a/web/cypress/fixtures/export-route-dataset.json b/web/cypress/fixtures/export-route-dataset.json index 2356f828b5..339b05e582 100644 --- a/web/cypress/fixtures/export-route-dataset.json +++ b/web/cypress/fixtures/export-route-dataset.json @@ -40,12 +40,17 @@ ], "timeout": { "connect": 6, - "read": 6, - "send": 6 + "send": 6, + "read": 6 }, "type": "roundrobin", "scheme": "http", - "pass_host": "pass" + "pass_host": "pass", + "keepalive_pool": { + "idle_timeout": 60, + "requests": 1000, + "size": 320 + } } } } @@ -92,12 +97,17 @@ ], "timeout": { "connect": 6, - "read": 6, - "send": 6 + "send": 6, + "read": 6 }, "type": "roundrobin", "scheme": "http", - "pass_host": "pass" + "pass_host": "pass", + "keepalive_pool": { + "idle_timeout": 60, + "requests": 1000, + "size": 320 + } } } }, @@ -143,12 +153,17 @@ ], "timeout": { "connect": 6, - "read": 6, - "send": 6 + "send": 6, + "read": 6 }, "type": "roundrobin", "scheme": "http", - "pass_host": "pass" + "pass_host": "pass", + "keepalive_pool": { + "idle_timeout": 60, + "requests": 1000, + "size": 320 + } } } } diff --git a/web/cypress/fixtures/plugin-dataset.json b/web/cypress/fixtures/plugin-dataset.json index 940f34af79..0ae63b98e3 100644 --- a/web/cypress/fixtures/plugin-dataset.json +++ b/web/cypress/fixtures/plugin-dataset.json @@ -291,12 +291,12 @@ { "shouldValid": true, "data": { - "allow_origins": "", - "allow_methods": "", - "allow_headers": "", - "expose_headers": "", - "max_age": 600, - "allow_credential": true + "allow_origins": "*", + "allow_methods": "*", + "allow_headers": "*", + "expose_headers": "*", + "max_age": 5, + "allow_credential": false } }, { @@ -506,7 +506,7 @@ } }, { - "shouldValid": true, + "shouldValid": false, "data": { "conn": 5, "burst": 1, @@ -516,7 +516,7 @@ } }, { - "shouldValid": true, + "shouldValid": false, "data": { "conn": 5, "burst": 1, @@ -693,13 +693,13 @@ "data": {} }, { - "shouldValid": false, + "shouldValid": true, "data": { "invalid": "invalid" } }, { - "shouldValid": false, + "shouldValid": true, "data": { "invalid_property": 1 } @@ -862,7 +862,7 @@ } }, { - "shouldValid": false, + "shouldValid": true, "data": { "uri": "/apisix/home", "host": "apisix.apache.org", @@ -1054,7 +1054,7 @@ } }, { - "shouldValid": false, + "shouldValid": true, "data": { "body": "Hello world", "headers": { diff --git a/web/cypress/integration/consumer/create-with-limit-conn-form.spec.js b/web/cypress/integration/consumer/create-with-limit-conn-form.spec.js index b4cac1d5a6..8eb41ae06d 100644 --- a/web/cypress/integration/consumer/create-with-limit-conn-form.spec.js +++ b/web/cypress/integration/consumer/create-with-limit-conn-form.spec.js @@ -30,6 +30,7 @@ context('Create and delete consumer with limit-conn plugin form', () => { conn: '#conn', burst: '#burst', default_conn_delay: '#default_conn_delay', + only_use_default_delay: '#only_use_default_delay', key: '#key', rejected_code: '#rejected_code', title: '[title="remote_addr"]', @@ -89,6 +90,7 @@ context('Create and delete consumer with limit-conn plugin form', () => { cy.get(selector.conn).type(data.conn); cy.get(selector.burst).type(data.burst); cy.get(selector.default_conn_delay).type(data.default_conn_delay); + cy.get(selector.only_use_default_delay).click(); cy.get(selector.key).click(); cy.get(selector.selectDropdown).should('be.visible'); cy.get(selector.title).click({ diff --git a/web/cypress/integration/upstream/create_and_delete_upstream.spec.js b/web/cypress/integration/upstream/create_and_delete_upstream.spec.js index 6798186823..70a5ebc2e8 100644 --- a/web/cypress/integration/upstream/create_and_delete_upstream.spec.js +++ b/web/cypress/integration/upstream/create_and_delete_upstream.spec.js @@ -246,4 +246,65 @@ context('Create and Delete Upstream', () => { cy.contains('button', 'Confirm').click(); cy.get(selector.notification).should('contain', data.deleteUpstreamSuccess); }); + + it('should create upstream with keepalive pool default value', function () { + cy.visit('/'); + cy.contains('Upstream').click(); + cy.contains('Create').click(); + + cy.get(selector.name).type(data.upstreamName); + cy.get(selector.description).type(data.description); + + cy.get(selector.nodes_0_host).type(data.ip1); + cy.get(selector.nodes_0_port).clear().type('7000'); + cy.get(selector.nodes_0_weight).clear().type(1); + + cy.get('#keepalive_pool_size').clear().type('1'); + cy.get('#keepalive_pool_idle_timeout').clear().type('15.5'); + cy.get('#keepalive_pool_requests').clear().type('50'); + + cy.contains('Next').click(); + cy.get(selector.input).should('be.disabled'); + cy.contains('Submit').click(); + cy.get(selector.notification).should('contain', data.createUpstreamSuccess); + cy.url().should('contains', 'upstream/list'); + }); + + it('should delete the upstream', function () { + cy.visit('/'); + cy.contains('Upstream').click(); + cy.contains(data.upstreamName).siblings().contains('Delete').click(); + cy.contains('button', 'Confirm').click(); + cy.get(selector.notification).should('contain', data.deleteUpstreamSuccess); + }); + + it('should create upstream with retries and retry timeout', function () { + cy.visit('/'); + cy.contains('Upstream').click(); + cy.contains('Create').click(); + + cy.get(selector.name).type(data.upstreamName); + cy.get(selector.description).type(data.description); + + cy.get(selector.nodes_0_host).type(data.ip1); + cy.get(selector.nodes_0_port).clear().type('7000'); + cy.get(selector.nodes_0_weight).clear().type(1); + + cy.get('#retries').clear().type('5'); + cy.get('#retry_timeout').clear().type('15.5'); + + cy.contains('Next').click(); + cy.get(selector.input).should('be.disabled'); + cy.contains('Submit').click(); + cy.get(selector.notification).should('contain', data.createUpstreamSuccess); + cy.url().should('contains', 'upstream/list'); + }); + + it('should delete the upstream', function () { + cy.visit('/'); + cy.contains('Upstream').click(); + cy.contains(data.upstreamName).siblings().contains('Delete').click(); + cy.contains('button', 'Confirm').click(); + cy.get(selector.notification).should('contain', data.deleteUpstreamSuccess); + }); }); diff --git a/web/src/components/Plugin/UI/limit-conn.tsx b/web/src/components/Plugin/UI/limit-conn.tsx index a680b7d8b8..9928e6b23c 100644 --- a/web/src/components/Plugin/UI/limit-conn.tsx +++ b/web/src/components/Plugin/UI/limit-conn.tsx @@ -16,7 +16,7 @@ */ import React from 'react'; import type { FormInstance } from 'antd/es/form'; -import { Form, InputNumber, Select } from 'antd'; +import { Form, InputNumber, Select, Switch } from 'antd'; import { useIntl } from 'umi'; type Props = { @@ -37,6 +37,9 @@ const FORM_ITEM_LAYOUT = { const LimitConn: React.FC = ({ form, schema }) => { const { formatMessage } = useIntl(); const propertires = schema?.properties; + const onlyUseDefaultDelay = form.getFieldValue('only_use_default_delay') + ? form.getFieldValue('only_use_default_delay') + : false; return (
= ({ form, schema }) => { + + + + = ({ form, schema }) => { })} - - - -
); }; diff --git a/web/src/components/Plugin/locales/en-US.ts b/web/src/components/Plugin/locales/en-US.ts index 689a6fc3c5..39bdb31880 100644 --- a/web/src/components/Plugin/locales/en-US.ts +++ b/web/src/components/Plugin/locales/en-US.ts @@ -85,6 +85,8 @@ export default { 'to limit the concurrency level. For example, one can use the host name (or server zone) as the key so that we limit concurrency per host name. Otherwise, we can also use the client address as the key so that we can avoid a single client from flooding our service with too many parallel connections or requests. Now accept those as key: "remote_addr"(client\'s IP), "server_addr"(server\'s IP), "X-Forwarded-For/X-Real-IP" in request header, "consumer_name"(consumer\'s username).', 'component.pluginForm.limit-conn.rejected_code.tooltip': 'returned when the request exceeds conn + burst will be rejected.', + 'component.pluginForm.limit-conn.only_use_default_delay.tooltip': + 'enable the strict mode of the latency seconds. If you set this option to true, it will run strictly according to the latency seconds you set without additional calculation logic.', // limit-req 'component.pluginForm.limit-req.rate.tooltip': diff --git a/web/src/components/Plugin/locales/zh-CN.ts b/web/src/components/Plugin/locales/zh-CN.ts index f7ef721cba..324ae28f2c 100644 --- a/web/src/components/Plugin/locales/zh-CN.ts +++ b/web/src/components/Plugin/locales/zh-CN.ts @@ -80,6 +80,9 @@ export default { '用户指定的限制并发级别的关键字,可以是客户端 IP 或服务端 IP。例如,可以使用主机名(或服务器区域)作为关键字,以便限制每个主机名的并发性。 否则,我们也可以使用客户端地址作为关键字,这样我们就可以避免单个客户端用太多的并行连接或请求淹没我们的服务。当前接受的 key 有:"remote_addr"(客户端 IP 地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP", "consumer_name"(consumer 的 username)。', 'component.pluginForm.limit-conn.rejected_code.tooltip': '当请求超过 conn + burst 这个阈值时,返回的 HTTP 状态码。', + 'component.pluginForm.limit-conn.only_use_default_delay.tooltip': + '延迟时间的严格模式。 如果设置为true的话,将会严格按照设置的时间来进行延迟', + // limit-req 'component.pluginForm.limit-req.rate.tooltip': '指定的请求速率(以秒为单位),请求速率超过 rate 但没有超过 (rate + brust)的请求会被加上延时。', diff --git a/web/src/components/Upstream/UpstreamForm.tsx b/web/src/components/Upstream/UpstreamForm.tsx index 3afb18b8f6..c12898a4b5 100644 --- a/web/src/components/Upstream/UpstreamForm.tsx +++ b/web/src/components/Upstream/UpstreamForm.tsx @@ -31,6 +31,8 @@ import PassHost from './components/PassHost'; import TLSComponent from './components/TLS'; import UpstreamType from './components/UpstreamType'; import { convertToRequestData } from './service'; +import RetryTimeout from './components/RetryTimeout'; +import KeepalivePool from './components/KeepalivePool'; type Upstream = { name?: string; @@ -270,6 +272,14 @@ const UpstreamForm: React.FC = forwardRef( ); }; + const KeepalivePoolComponent = () => { + return ( + + + + ); + }; + return (
{showSelector && ( @@ -291,6 +301,7 @@ const UpstreamForm: React.FC = forwardRef( + prev.scheme !== next.scheme}> @@ -307,6 +318,8 @@ const UpstreamForm: React.FC = forwardRef( ))} + + )} diff --git a/web/src/components/Upstream/components/KeepalivePool.tsx b/web/src/components/Upstream/components/KeepalivePool.tsx new file mode 100644 index 0000000000..f1fcc984af --- /dev/null +++ b/web/src/components/Upstream/components/KeepalivePool.tsx @@ -0,0 +1,89 @@ +/* + * 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 from 'react'; +import { Row, Col, Form, InputNumber } from 'antd'; +import { useIntl } from 'umi'; + +type Props = { + readonly?: boolean; +}; + +const KeepalivePool: React.FC = ({ readonly }) => { + const { formatMessage } = useIntl(); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default KeepalivePool; diff --git a/web/src/components/Upstream/components/RetryTimeout.tsx b/web/src/components/Upstream/components/RetryTimeout.tsx new file mode 100644 index 0000000000..2fab431dc6 --- /dev/null +++ b/web/src/components/Upstream/components/RetryTimeout.tsx @@ -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. + */ +import React from 'react'; +import { Form, InputNumber } from 'antd'; +import { useIntl } from 'umi'; + +type Props = { + readonly?: boolean; +}; + +const RetryTimeout: React.FC = ({ readonly }) => { + const { formatMessage } = useIntl(); + + return ( + + + + + + ); +}; + +export default RetryTimeout; diff --git a/web/src/components/Upstream/locales/en-US.ts b/web/src/components/Upstream/locales/en-US.ts index c8b8b90062..b449349768 100644 --- a/web/src/components/Upstream/locales/en-US.ts +++ b/web/src/components/Upstream/locales/en-US.ts @@ -57,6 +57,20 @@ export default { 'component.upstream.fields.retries.tooltip': 'The retry mechanism sends the request to the next upstream node. A value of 0 disables the retry mechanism and leaves the table empty to use the number of available backend nodes.', + 'component.upstream.fields.retry_timeout': 'Retry Timeout', + 'component.upstream.fields.retry_timeout.tooltip': + 'Configure a number to limit the amount of seconds that retries can be continued, and do not continue retries if the previous request and retry requests have taken too long. 0 means disable retry timeout mechanism.', + + 'component.upstream.fields.keepalive_pool': 'Keepalive Pool', + 'component.upstream.fields.keepalive_pool.tooltip': 'Set independent keepalive pool for Upstream', + 'component.upstream.fields.keepalive_pool.size': 'Size', + 'component.upstream.fields.keepalive_pool.size.placeholder': 'Please enter the size', + 'component.upstream.fields.keepalive_pool.idle_timeout': 'Idle Timeout', + 'component.upstream.fields.keepalive_pool.idle_timeout.placeholder': + 'Please enter the idle timeout', + 'component.upstream.fields.keepalive_pool.requests': 'Requests', + 'component.upstream.fields.keepalive_pool.requests.placeholder': 'Please enter the requests', + 'component.upstream.fields.checks.active.type': 'Type', 'component.upstream.fields.checks.active.type.tooltip': 'Whether to perform active health checks using HTTP or HTTPS, or just attempt a TCP connection.', diff --git a/web/src/components/Upstream/locales/zh-CN.ts b/web/src/components/Upstream/locales/zh-CN.ts index 1adb63fc70..4e1dc1001f 100644 --- a/web/src/components/Upstream/locales/zh-CN.ts +++ b/web/src/components/Upstream/locales/zh-CN.ts @@ -56,6 +56,19 @@ export default { 'component.upstream.fields.retries.tooltip': '重试机制将请求发到下一个上游节点。值为 0 表示禁用重试机制,留空表示使用可用后端节点的数量。', + 'component.upstream.fields.retry_timeout': '重试超时时间', + 'component.upstream.fields.retry_timeout.tooltip': + '限制是否继续重试的时间,若之前的请求和重试请求花费太多时间就不再继续重试。0 代表不启用重试超时机制。', + + 'component.upstream.fields.keepalive_pool': '连接池', + 'component.upstream.fields.keepalive_pool.tooltip': '为 upstream 对象设置独立的连接池', + 'component.upstream.fields.keepalive_pool.size': '容量', + 'component.upstream.fields.keepalive_pool.size.placeholder': '请输入容量', + 'component.upstream.fields.keepalive_pool.idle_timeout': '空闲超时时间', + 'component.upstream.fields.keepalive_pool.idle_timeout.placeholder': '请输入空闲超时时间', + 'component.upstream.fields.keepalive_pool.requests': '请求数量', + 'component.upstream.fields.keepalive_pool.requests.placeholder': '请输入请求数量', + 'component.upstream.fields.checks.active.type': '类型', 'component.upstream.fields.checks.active.type.tooltip': '是使用 HTTP 或 HTTPS 进行主动健康检查,还是只尝试 TCP 连接。', diff --git a/web/src/pages/Upstream/locales/en-US.ts b/web/src/pages/Upstream/locales/en-US.ts index 8cbd7988e2..424df4e832 100644 --- a/web/src/pages/Upstream/locales/en-US.ts +++ b/web/src/pages/Upstream/locales/en-US.ts @@ -65,6 +65,7 @@ export default { 'page.upstream.step.input.healthyCheck.passive.http_statuses': 'Please enter http status', 'page.upstream.step.healthyCheck.passive.tcp_failures': 'TCP Failures', 'page.upstream.step.input.healthyCheck.passive.tcp_failures': 'Please enter TCP failures', + 'page.upstream.step.keepalive_pool': 'Keepalive Pool', 'page.upstream.notificationMessage.enableHealthCheckFirst': 'Please enable health check first.', 'page.upstream.upstream_host.required': 'Please enter the custom Host', diff --git a/web/src/pages/Upstream/locales/zh-CN.ts b/web/src/pages/Upstream/locales/zh-CN.ts index 2db82eb20d..b2d6778f77 100644 --- a/web/src/pages/Upstream/locales/zh-CN.ts +++ b/web/src/pages/Upstream/locales/zh-CN.ts @@ -64,6 +64,7 @@ export default { 'page.upstream.step.input.healthyCheck.passive.http_statuses': '请输入状态码', 'page.upstream.step.healthyCheck.passive.tcp_failures': 'TCP 失败次数', 'page.upstream.step.input.healthyCheck.passive.tcp_failures': '请输入 TCP 失败次数', + 'page.upstream.step.keepalive_pool': '连接池', 'page.upstream.notificationMessage.enableHealthCheckFirst': '请先启用探活健康检查。', 'page.upstream.upstream_host.required': '请输入自定义 Host 请求头', diff --git a/web/src/pages/Upstream/typing.d.ts b/web/src/pages/Upstream/typing.d.ts index 3a7d03e84e..5c73cc70d2 100644 --- a/web/src/pages/Upstream/typing.d.ts +++ b/web/src/pages/Upstream/typing.d.ts @@ -23,6 +23,12 @@ declare namespace UpstreamModule { namespace_id?: string; }; + type KeepalivePool = { + size?: number; + idle_timeout?: number; + requests?: number; + }; + type Timeout = Record<'connect' | 'send' | 'read', number>; type HealthCheck = { @@ -81,12 +87,14 @@ declare namespace UpstreamModule { key?: string; checks?: HealthCheck; retries?: number; + retry_timeout?: number; enable_websocket?: boolean; timeout?: Timeout; name?: string; desc?: string; pass_host?: 'pass' | 'node' | 'rewrite'; upstream_host: UpstreamHost[]; + keepalive_pool: KeepalivePool; // Custom Fields that need to be omitted custom?: {};