This is an automated email from the ASF dual-hosted git repository.
LiteSun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 597d14011 test: add gateway-schema contract test for the zod resource
schemas (#3449)
597d14011 is described below
commit 597d14011088d15a464149ae7b9e93621eef9114
Author: Yuhan <[email protected]>
AuthorDate: Tue Jul 28 14:28:04 2026 +0800
test: add gateway-schema contract test for the zod resource schemas (#3449)
---
package.json | 1 +
scripts/refresh-gateway-schema.mjs | 117 +++
.../schema/apisix/__fixtures__/gateway/_meta.json | 17 +
.../apisix/__fixtures__/gateway/consumer.json | 69 ++
.../__fixtures__/gateway/consumer_group.json | 55 ++
.../apisix/__fixtures__/gateway/credential.json | 62 ++
.../apisix/__fixtures__/gateway/global_rule.json | 33 +
.../apisix/__fixtures__/gateway/plugin_config.json | 53 ++
.../schema/apisix/__fixtures__/gateway/proto.json | 56 ++
.../schema/apisix/__fixtures__/gateway/route.json | 939 +++++++++++++++++++++
.../apisix/__fixtures__/gateway/service.json | 605 +++++++++++++
.../schema/apisix/__fixtures__/gateway/ssl.json | 172 ++++
.../apisix/__fixtures__/gateway/stream_route.json | 717 ++++++++++++++++
.../apisix/__fixtures__/gateway/upstream.json | 522 ++++++++++++
src/types/schema/apisix/gateway-contract.test.ts | 171 ++++
15 files changed, 3589 insertions(+)
diff --git a/package.json b/package.json
index 29f22f931..217d055fd 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"lint:fix": "pnpm lint --fix",
"preview": "vite preview",
"test": "vitest run src",
+ "refresh:gateway-schema": "node scripts/refresh-gateway-schema.mjs",
"prepare": "husky",
"e2e": "playwright test"
},
diff --git a/scripts/refresh-gateway-schema.mjs
b/scripts/refresh-gateway-schema.mjs
new file mode 100644
index 000000000..4802bbba2
--- /dev/null
+++ b/scripts/refresh-gateway-schema.mjs
@@ -0,0 +1,117 @@
+/**
+ * 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.
+ */
+
+/*
+ * Refreshes the committed snapshot of APISIX's own per-resource JSON schemas,
+ * used by gateway-contract.test.ts to assert the dashboard's zod layer is not
+ * stricter than the gateway.
+ *
+ * Run this against a running APISIX Admin API and commit the result whenever
+ * the pinned APISIX version changes:
+ *
+ * pnpm refresh:gateway-schema
+ *
+ * Defaults target the local prod-like stack; override with env vars:
+ * ADMIN_URL=http://127.0.0.1:9180 ADMIN_KEY=<key> pnpm
refresh:gateway-schema
+ */
+import { execSync } from 'node:child_process';
+import { mkdirSync, writeFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ADMIN_URL = process.env.ADMIN_URL ?? 'http://127.0.0.1:9180';
+const ADMIN_KEY = process.env.ADMIN_KEY ?? 'edd1c9f034335f136f87ad84b625c8f1';
+
+// Resources whose write schema the gateway exposes at
+// GET /apisix/admin/schema/<resource>. `secret` is omitted: it is schema'd
+// per-manager and the bare endpoint returns 400.
+const RESOURCES = [
+ 'route',
+ 'service',
+ 'upstream',
+ 'consumer',
+ 'ssl',
+ 'global_rule',
+ 'plugin_config',
+ 'consumer_group',
+ 'stream_route',
+ 'proto',
+ 'credential',
+];
+
+const OUT_DIR = join(
+ dirname(fileURLToPath(import.meta.url)),
+ '..',
+ 'src',
+ 'types',
+ 'schema',
+ 'apisix',
+ '__fixtures__',
+ 'gateway'
+);
+
+const fetchSchema = async (resource) => {
+ const res = await fetch(`${ADMIN_URL}/apisix/admin/schema/${resource}`, {
+ headers: { 'X-API-KEY': ADMIN_KEY },
+ });
+ if (!res.ok) {
+ throw new Error(`GET schema/${resource} -> ${res.status}`);
+ }
+ return res.json();
+};
+
+const apisixVersion = () => {
+ try {
+ const out = execSync(
+ 'docker exec apisix-prod-apisix-1 apisix version 2>/dev/null',
+ { encoding: 'utf8' }
+ );
+ return out.trim().split(/\s+/).pop() ?? 'unknown';
+ } catch {
+ return process.env.APISIX_VERSION ?? 'unknown';
+ }
+};
+
+const main = async () => {
+ mkdirSync(OUT_DIR, { recursive: true });
+ for (const resource of RESOURCES) {
+ const schema = await fetchSchema(resource);
+ writeFileSync(
+ join(OUT_DIR, `${resource}.json`),
+ JSON.stringify(schema, null, 2) + '\n'
+ );
+ console.log(`wrote ${resource}.json`);
+ }
+ writeFileSync(
+ join(OUT_DIR, '_meta.json'),
+ JSON.stringify(
+ {
+ apisixVersion: apisixVersion(),
+ resources: RESOURCES,
+ note: 'Generated by scripts/refresh-gateway-schema.mjs. Do not edit by
hand; re-run on APISIX version bumps.',
+ },
+ null,
+ 2
+ ) + '\n'
+ );
+ console.log(`wrote _meta.json (${RESOURCES.length} resources)`);
+};
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/src/types/schema/apisix/__fixtures__/gateway/_meta.json
b/src/types/schema/apisix/__fixtures__/gateway/_meta.json
new file mode 100644
index 000000000..1b9a3799a
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/_meta.json
@@ -0,0 +1,17 @@
+{
+ "apisixVersion": "3.17.0",
+ "resources": [
+ "route",
+ "service",
+ "upstream",
+ "consumer",
+ "ssl",
+ "global_rule",
+ "plugin_config",
+ "consumer_group",
+ "stream_route",
+ "proto",
+ "credential"
+ ],
+ "note": "Generated by scripts/refresh-gateway-schema.mjs. Do not edit by
hand; re-run on APISIX version bumps."
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/consumer.json
b/src/types/schema/apisix/__fixtures__/gateway/consumer.json
new file mode 100644
index 000000000..500a93b3c
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/consumer.json
@@ -0,0 +1,69 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "username"
+ ],
+ "properties": {
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "group_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "plugins": {
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9_\\-]+$",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/consumer_group.json
b/src/types/schema/apisix/__fixtures__/gateway/consumer_group.json
new file mode 100644
index 000000000..31a1085e4
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/consumer_group.json
@@ -0,0 +1,55 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "plugins"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "plugins": {
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/credential.json
b/src/types/schema/apisix/__fixtures__/gateway/credential.json
new file mode 100644
index 000000000..34b9a6ac8
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/credential.json
@@ -0,0 +1,62 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "plugins": {
+ "type": "object",
+ "maxProperties": 1
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "id": {
+ "oneOf": [
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_]+/credentials/[a-zA-Z0-9-_.]+$",
+ "maxLength": 128,
+ "minLength": 15
+ }
+ ]
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/global_rule.json
b/src/types/schema/apisix/__fixtures__/gateway/global_rule.json
new file mode 100644
index 000000000..4fb07e914
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/global_rule.json
@@ -0,0 +1,33 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "plugins"
+ ],
+ "properties": {
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "plugins": {
+ "type": "object"
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/plugin_config.json
b/src/types/schema/apisix/__fixtures__/gateway/plugin_config.json
new file mode 100644
index 000000000..f9b748156
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/plugin_config.json
@@ -0,0 +1,53 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "plugins"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "plugins": {
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/proto.json
b/src/types/schema/apisix/__fixtures__/gateway/proto.json
new file mode 100644
index 000000000..12b0df2e5
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/proto.json
@@ -0,0 +1,56 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "content"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "content": {
+ "type": "string",
+ "maxLength": 1048576,
+ "minLength": 1
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/route.json
b/src/types/schema/apisix/__fixtures__/gateway/route.json
new file mode 100644
index 000000000..3acea8690
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/route.json
@@ -0,0 +1,939 @@
+{
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "plugins",
+ "uri"
+ ]
+ },
+ {
+ "required": [
+ "upstream",
+ "uri"
+ ]
+ },
+ {
+ "required": [
+ "upstream_id",
+ "uri"
+ ]
+ },
+ {
+ "required": [
+ "service_id",
+ "uri"
+ ]
+ },
+ {
+ "required": [
+ "plugins",
+ "uris"
+ ]
+ },
+ {
+ "required": [
+ "upstream",
+ "uris"
+ ]
+ },
+ {
+ "required": [
+ "upstream_id",
+ "uris"
+ ]
+ },
+ {
+ "required": [
+ "service_id",
+ "uris"
+ ]
+ },
+ {
+ "required": [
+ "script",
+ "uri"
+ ]
+ },
+ {
+ "required": [
+ "script",
+ "uris"
+ ]
+ }
+ ],
+ "properties": {
+ "remote_addr": {
+ "description": "client IP",
+ "anyOf": [
+ {
+ "type": "string",
+ "title": "IPv4",
+ "format": "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",
+ "title": "IPv6",
+ "format": "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"
+ },
+ "remote_addrs": {
+ "type": "array",
+ "items": {
+ "description": "client IP",
+ "anyOf": [
+ {
+ "type": "string",
+ "title": "IPv4",
+ "format": "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",
+ "title": "IPv6",
+ "format": "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,
+ "uniqueItems": true
+ },
+ "filter_func": {
+ "type": "string",
+ "pattern": "^function",
+ "minLength": 10
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "enable_websocket": {
+ "description": "enable websocket for request",
+ "type": "boolean"
+ },
+ "upstream": {
+ "type": "object",
+ "oneOf": [
+ {
+ "required": [
+ "nodes"
+ ]
+ },
+ {
+ "required": [
+ "service_name",
+ "discovery_type"
+ ]
+ }
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "hash_on": {
+ "type": "string",
+ "default": "vars",
+ "enum": [
+ "vars",
+ "header",
+ "cookie",
+ "consumer",
+ "vars_combinations"
+ ]
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "upstream_host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "nodes": {
+ "anyOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "host",
+ "weight"
+ ],
+ "properties": {
+ "priority": {
+ "description": "priority of node",
+ "type": "integer",
+ "default": 0
+ },
+ "weight": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ },
+ "port": {
+ "description": "port of node",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "metadata": {
+ "description": "metadata of node",
+ "type": "object"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "type": {
+ "description": "algorithms of load balancing",
+ "type": "string",
+ "default": "roundrobin"
+ },
+ "discovery_type": {
+ "description": "discovery type",
+ "type": "string"
+ },
+ "discovery_args": {
+ "type": "object",
+ "properties": {
+ "namespace_id": {
+ "description": "namespace id",
+ "type": "string"
+ },
+ "group_name": {
+ "description": "group name",
+ "type": "string"
+ }
+ }
+ },
+ "timeout": {
+ "type": "object",
+ "required": [
+ "connect",
+ "send",
+ "read"
+ ],
+ "properties": {
+ "send": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "connect": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "read": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ }
+ }
+ },
+ "key": {
+ "description": "the key of chash for dynamic load balancing",
+ "type": "string"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "checks": {
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "active"
+ ]
+ },
+ {
+ "required": [
+ "active",
+ "passive"
+ ]
+ }
+ ],
+ "properties": {
+ "active": {
+ "type": "object",
+ "properties": {
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ },
+ "timeouts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 3
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 404,
+ 500,
+ 501,
+ 502,
+ 503,
+ 504,
+ 505
+ ],
+ "uniqueItems": true
+ }
+ }
+ },
+ "port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "req_headers": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "uniqueItems": true
+ },
+ "minItems": 1
+ },
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "timeout": {
+ "type": "number",
+ "default": 1
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 302
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "concurrency": {
+ "type": "integer",
+ "default": 10
+ },
+ "http_path": {
+ "type": "string",
+ "default": "/"
+ },
+ "https_verify_certificate": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "passive": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "timeouts": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 7
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 500,
+ 503
+ ],
+ "uniqueItems": true
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 201,
+ 202,
+ 203,
+ 204,
+ 205,
+ 206,
+ 207,
+ 208,
+ 226,
+ 300,
+ 301,
+ 302,
+ 303,
+ 304,
+ 305,
+ 306,
+ 307,
+ 308
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "retry_timeout": {
+ "type": "number",
+ "minimum": 0
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "tls": {
+ "type": "object",
+ "properties": {
+ "client_key": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 64
+ },
+ "client_cert": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 128
+ },
+ "client_cert_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "verify": {
+ "type": "boolean",
+ "description": "Turn on server certificate verification,
currently only kafka upstream is supported",
+ "default": false
+ }
+ },
+ "dependencies": {
+ "client_key": {
+ "required": [
+ "client_cert"
+ ]
+ },
+ "client_cert": {
+ "required": [
+ "client_key"
+ ]
+ },
+ "client_cert_id": {
+ "not": {
+ "required": [
+ "client_cert",
+ "client_key"
+ ]
+ }
+ }
+ }
+ },
+ "retries": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "keepalive_pool": {
+ "type": "object",
+ "properties": {
+ "idle_timeout": {
+ "type": "number",
+ "default": 60,
+ "minimum": 0
+ },
+ "requests": {
+ "type": "integer",
+ "default": 1000,
+ "minimum": 1
+ },
+ "size": {
+ "type": "integer",
+ "default": 320,
+ "minimum": 1
+ }
+ }
+ },
+ "service_name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "scheme": {
+ "default": "http",
+ "description": "The scheme of the upstream. For L7 proxy, it can be
one of grpc/grpcs/http/https. For L4 proxy, it can be one of tcp/tls/udp. For
specific protocols, it can be kafka.",
+ "enum": [
+ "grpc",
+ "grpcs",
+ "http",
+ "https",
+ "tcp",
+ "tls",
+ "udp",
+ "kafka"
+ ]
+ },
+ "pass_host": {
+ "description": "mod of host passing",
+ "default": "pass",
+ "type": "string",
+ "enum": [
+ "pass",
+ "node",
+ "rewrite"
+ ]
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+ },
+ "upstream_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "script": {
+ "type": "string",
+ "maxLength": 102400,
+ "minLength": 10
+ },
+ "timeout": {
+ "type": "object",
+ "required": [
+ "connect",
+ "send",
+ "read"
+ ],
+ "properties": {
+ "send": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "connect": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "read": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ }
+ }
+ },
+ "hosts": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "script_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "uris": {
+ "type": "array",
+ "items": {
+ "description": "HTTP uri",
+ "type": "string"
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "uri": {
+ "type": "string",
+ "maxLength": 4096,
+ "minLength": 1
+ },
+ "plugin_config_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "vars": {
+ "type": "array"
+ },
+ "status": {
+ "description": "route status, 1 to enable, 0 to disable",
+ "default": 1,
+ "type": "integer",
+ "enum": [
+ 1,
+ 0
+ ]
+ },
+ "priority": {
+ "type": "integer",
+ "default": 0
+ },
+ "plugins": {
+ "type": "object"
+ },
+ "service_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "methods": {
+ "type": "array",
+ "items": {
+ "description": "HTTP method",
+ "type": "string",
+ "enum": [
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE",
+ "PATCH",
+ "HEAD",
+ "OPTIONS",
+ "CONNECT",
+ "TRACE",
+ "PURGE"
+ ]
+ },
+ "uniqueItems": true
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ },
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "script",
+ "plugins"
+ ]
+ },
+ {
+ "required": [
+ "script",
+ "plugin_config_id"
+ ]
+ }
+ ]
+ },
+ "allOf": [
+ {
+ "oneOf": [
+ {
+ "required": [
+ "uri"
+ ]
+ },
+ {
+ "required": [
+ "uris"
+ ]
+ }
+ ]
+ },
+ {
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "host"
+ ]
+ },
+ {
+ "required": [
+ "hosts"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "required": [
+ "host"
+ ]
+ },
+ {
+ "required": [
+ "hosts"
+ ]
+ }
+ ]
+ },
+ {
+ "oneOf": [
+ {
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "remote_addr"
+ ]
+ },
+ {
+ "required": [
+ "remote_addrs"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "required": [
+ "remote_addr"
+ ]
+ },
+ {
+ "required": [
+ "remote_addrs"
+ ]
+ }
+ ]
+ }
+ ],
+ "additionalProperties": false
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/service.json
b/src/types/schema/apisix/__fixtures__/gateway/service.json
new file mode 100644
index 000000000..a1e6fc620
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/service.json
@@ -0,0 +1,605 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "upstream": {
+ "type": "object",
+ "oneOf": [
+ {
+ "required": [
+ "nodes"
+ ]
+ },
+ {
+ "required": [
+ "service_name",
+ "discovery_type"
+ ]
+ }
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "hash_on": {
+ "type": "string",
+ "default": "vars",
+ "enum": [
+ "vars",
+ "header",
+ "cookie",
+ "consumer",
+ "vars_combinations"
+ ]
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "upstream_host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "nodes": {
+ "anyOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "host",
+ "weight"
+ ],
+ "properties": {
+ "priority": {
+ "description": "priority of node",
+ "type": "integer",
+ "default": 0
+ },
+ "weight": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ },
+ "port": {
+ "description": "port of node",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "metadata": {
+ "description": "metadata of node",
+ "type": "object"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "type": {
+ "description": "algorithms of load balancing",
+ "type": "string",
+ "default": "roundrobin"
+ },
+ "discovery_type": {
+ "description": "discovery type",
+ "type": "string"
+ },
+ "discovery_args": {
+ "type": "object",
+ "properties": {
+ "namespace_id": {
+ "description": "namespace id",
+ "type": "string"
+ },
+ "group_name": {
+ "description": "group name",
+ "type": "string"
+ }
+ }
+ },
+ "timeout": {
+ "type": "object",
+ "required": [
+ "connect",
+ "send",
+ "read"
+ ],
+ "properties": {
+ "send": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "connect": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "read": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ }
+ }
+ },
+ "key": {
+ "description": "the key of chash for dynamic load balancing",
+ "type": "string"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "checks": {
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "active"
+ ]
+ },
+ {
+ "required": [
+ "active",
+ "passive"
+ ]
+ }
+ ],
+ "properties": {
+ "active": {
+ "type": "object",
+ "properties": {
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ },
+ "timeouts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 3
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 404,
+ 500,
+ 501,
+ 502,
+ 503,
+ 504,
+ 505
+ ],
+ "uniqueItems": true
+ }
+ }
+ },
+ "port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "req_headers": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "uniqueItems": true
+ },
+ "minItems": 1
+ },
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "timeout": {
+ "type": "number",
+ "default": 1
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 302
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "concurrency": {
+ "type": "integer",
+ "default": 10
+ },
+ "http_path": {
+ "type": "string",
+ "default": "/"
+ },
+ "https_verify_certificate": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "passive": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "timeouts": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 7
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 500,
+ 503
+ ],
+ "uniqueItems": true
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 201,
+ 202,
+ 203,
+ 204,
+ 205,
+ 206,
+ 207,
+ 208,
+ 226,
+ 300,
+ 301,
+ 302,
+ 303,
+ 304,
+ 305,
+ 306,
+ 307,
+ 308
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "retry_timeout": {
+ "type": "number",
+ "minimum": 0
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "tls": {
+ "type": "object",
+ "properties": {
+ "client_key": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 64
+ },
+ "client_cert": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 128
+ },
+ "client_cert_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "verify": {
+ "type": "boolean",
+ "description": "Turn on server certificate verification,
currently only kafka upstream is supported",
+ "default": false
+ }
+ },
+ "dependencies": {
+ "client_key": {
+ "required": [
+ "client_cert"
+ ]
+ },
+ "client_cert": {
+ "required": [
+ "client_key"
+ ]
+ },
+ "client_cert_id": {
+ "not": {
+ "required": [
+ "client_cert",
+ "client_key"
+ ]
+ }
+ }
+ }
+ },
+ "retries": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "keepalive_pool": {
+ "type": "object",
+ "properties": {
+ "idle_timeout": {
+ "type": "number",
+ "default": 60,
+ "minimum": 0
+ },
+ "requests": {
+ "type": "integer",
+ "default": 1000,
+ "minimum": 1
+ },
+ "size": {
+ "type": "integer",
+ "default": 320,
+ "minimum": 1
+ }
+ }
+ },
+ "service_name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "scheme": {
+ "default": "http",
+ "description": "The scheme of the upstream. For L7 proxy, it can be
one of grpc/grpcs/http/https. For L4 proxy, it can be one of tcp/tls/udp. For
specific protocols, it can be kafka.",
+ "enum": [
+ "grpc",
+ "grpcs",
+ "http",
+ "https",
+ "tcp",
+ "tls",
+ "udp",
+ "kafka"
+ ]
+ },
+ "pass_host": {
+ "description": "mod of host passing",
+ "default": "pass",
+ "type": "string",
+ "enum": [
+ "pass",
+ "node",
+ "rewrite"
+ ]
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+ },
+ "update_time": {
+ "type": "integer"
+ },
+ "upstream_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "script": {
+ "type": "string",
+ "maxLength": 102400,
+ "minLength": 10
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "plugins": {
+ "type": "object"
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "enable_websocket": {
+ "description": "enable websocket for request",
+ "type": "boolean"
+ },
+ "hosts": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/ssl.json
b/src/types/schema/apisix/__fixtures__/gateway/ssl.json
new file mode 100644
index 000000000..85880a367
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/ssl.json
@@ -0,0 +1,172 @@
+{
+ "type": "object",
+ "if": {
+ "properties": {
+ "type": {
+ "enum": [
+ "server"
+ ]
+ }
+ }
+ },
+ "else": {
+ "required": [
+ "key",
+ "cert"
+ ]
+ },
+ "properties": {
+ "update_time": {
+ "type": "integer"
+ },
+ "key": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 64
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "ssl status, 1 to enable, 0 to disable",
+ "default": 1,
+ "type": "integer",
+ "enum": [
+ 1,
+ 0
+ ]
+ },
+ "sni": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "type": {
+ "description": "ssl certificate type, server to server certificate,
client to client certificate for upstream",
+ "type": "string",
+ "default": "server",
+ "enum": [
+ "server",
+ "client"
+ ]
+ },
+ "cert": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 128
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "ssl_protocols": {
+ "type": "array",
+ "items": {
+ "enum": [
+ "TLSv1.1",
+ "TLSv1.2",
+ "TLSv1.3"
+ ]
+ },
+ "maxItems": 3,
+ "description": "set ssl protocols",
+ "uniqueItems": true
+ },
+ "keys": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 64
+ }
+ },
+ "client": {
+ "type": "object",
+ "required": [
+ "ca"
+ ],
+ "properties": {
+ "ca": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 128
+ },
+ "skip_mtls_uri_regex": {
+ "type": "array",
+ "items": {
+ "description": "uri regular expression to skip mtls",
+ "type": "string"
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "depth": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 1
+ }
+ }
+ },
+ "snis": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "minItems": 1
+ },
+ "certs": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 128
+ }
+ }
+ },
+ "additionalProperties": false,
+ "then": {
+ "oneOf": [
+ {
+ "required": [
+ "sni",
+ "key",
+ "cert"
+ ]
+ },
+ {
+ "required": [
+ "snis",
+ "key",
+ "cert"
+ ]
+ }
+ ]
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/stream_route.json
b/src/types/schema/apisix/__fixtures__/gateway/stream_route.json
new file mode 100644
index 000000000..9f52dd74a
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/stream_route.json
@@ -0,0 +1,717 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "remote_addr": {
+ "description": "client IP",
+ "anyOf": [
+ {
+ "type": "string",
+ "title": "IPv4",
+ "format": "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",
+ "title": "IPv6",
+ "format": "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"
+ },
+ "server_port": {
+ "description": "server port",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "update_time": {
+ "type": "integer"
+ },
+ "sni": {
+ "description": "server name indication",
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "upstream_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "server_addr": {
+ "description": "server IP",
+ "anyOf": [
+ {
+ "type": "string",
+ "title": "IPv4",
+ "format": "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",
+ "title": "IPv6",
+ "format": "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"
+ },
+ "protocol": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "conf": {
+ "description": "protocol-specific configuration",
+ "type": "object"
+ },
+ "superior_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "logger": {
+ "type": "array",
+ "items": {
+ "properties": {
+ "conf": {
+ "description": "logger plugin configuration",
+ "type": "object"
+ },
+ "filter": {
+ "description": "logger filter rules",
+ "type": "array"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "dependencies": {
+ "name": [
+ "conf"
+ ]
+ }
+ }
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "plugins": {
+ "type": "object"
+ },
+ "service_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "upstream": {
+ "type": "object",
+ "oneOf": [
+ {
+ "required": [
+ "nodes"
+ ]
+ },
+ {
+ "required": [
+ "service_name",
+ "discovery_type"
+ ]
+ }
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "hash_on": {
+ "type": "string",
+ "default": "vars",
+ "enum": [
+ "vars",
+ "header",
+ "cookie",
+ "consumer",
+ "vars_combinations"
+ ]
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "upstream_host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "nodes": {
+ "anyOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "host",
+ "weight"
+ ],
+ "properties": {
+ "priority": {
+ "description": "priority of node",
+ "type": "integer",
+ "default": 0
+ },
+ "weight": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ },
+ "port": {
+ "description": "port of node",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "metadata": {
+ "description": "metadata of node",
+ "type": "object"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "type": {
+ "description": "algorithms of load balancing",
+ "type": "string",
+ "default": "roundrobin"
+ },
+ "discovery_type": {
+ "description": "discovery type",
+ "type": "string"
+ },
+ "discovery_args": {
+ "type": "object",
+ "properties": {
+ "namespace_id": {
+ "description": "namespace id",
+ "type": "string"
+ },
+ "group_name": {
+ "description": "group name",
+ "type": "string"
+ }
+ }
+ },
+ "timeout": {
+ "type": "object",
+ "required": [
+ "connect",
+ "send",
+ "read"
+ ],
+ "properties": {
+ "send": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "connect": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "read": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ }
+ }
+ },
+ "key": {
+ "description": "the key of chash for dynamic load balancing",
+ "type": "string"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "checks": {
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "active"
+ ]
+ },
+ {
+ "required": [
+ "active",
+ "passive"
+ ]
+ }
+ ],
+ "properties": {
+ "active": {
+ "type": "object",
+ "properties": {
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ },
+ "timeouts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 3
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 404,
+ 500,
+ 501,
+ 502,
+ 503,
+ 504,
+ 505
+ ],
+ "uniqueItems": true
+ }
+ }
+ },
+ "port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "req_headers": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "uniqueItems": true
+ },
+ "minItems": 1
+ },
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "timeout": {
+ "type": "number",
+ "default": 1
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 302
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "concurrency": {
+ "type": "integer",
+ "default": 10
+ },
+ "http_path": {
+ "type": "string",
+ "default": "/"
+ },
+ "https_verify_certificate": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "passive": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "timeouts": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 7
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 500,
+ 503
+ ],
+ "uniqueItems": true
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 201,
+ 202,
+ 203,
+ 204,
+ 205,
+ 206,
+ 207,
+ 208,
+ 226,
+ 300,
+ 301,
+ 302,
+ 303,
+ 304,
+ 305,
+ 306,
+ 307,
+ 308
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "retry_timeout": {
+ "type": "number",
+ "minimum": 0
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "tls": {
+ "type": "object",
+ "properties": {
+ "client_key": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 64
+ },
+ "client_cert": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 128
+ },
+ "client_cert_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "verify": {
+ "type": "boolean",
+ "description": "Turn on server certificate verification,
currently only kafka upstream is supported",
+ "default": false
+ }
+ },
+ "dependencies": {
+ "client_key": {
+ "required": [
+ "client_cert"
+ ]
+ },
+ "client_cert": {
+ "required": [
+ "client_key"
+ ]
+ },
+ "client_cert_id": {
+ "not": {
+ "required": [
+ "client_cert",
+ "client_key"
+ ]
+ }
+ }
+ }
+ },
+ "retries": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "keepalive_pool": {
+ "type": "object",
+ "properties": {
+ "idle_timeout": {
+ "type": "number",
+ "default": 60,
+ "minimum": 0
+ },
+ "requests": {
+ "type": "integer",
+ "default": 1000,
+ "minimum": 1
+ },
+ "size": {
+ "type": "integer",
+ "default": 320,
+ "minimum": 1
+ }
+ }
+ },
+ "service_name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "scheme": {
+ "default": "http",
+ "description": "The scheme of the upstream. For L7 proxy, it can be
one of grpc/grpcs/http/https. For L4 proxy, it can be one of tcp/tls/udp. For
specific protocols, it can be kafka.",
+ "enum": [
+ "grpc",
+ "grpcs",
+ "http",
+ "https",
+ "tcp",
+ "tls",
+ "udp",
+ "kafka"
+ ]
+ },
+ "pass_host": {
+ "description": "mod of host passing",
+ "default": "pass",
+ "type": "string",
+ "enum": [
+ "pass",
+ "node",
+ "rewrite"
+ ]
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ }
+ }
+}
diff --git a/src/types/schema/apisix/__fixtures__/gateway/upstream.json
b/src/types/schema/apisix/__fixtures__/gateway/upstream.json
new file mode 100644
index 000000000..f4be357bc
--- /dev/null
+++ b/src/types/schema/apisix/__fixtures__/gateway/upstream.json
@@ -0,0 +1,522 @@
+{
+ "type": "object",
+ "oneOf": [
+ {
+ "required": [
+ "nodes"
+ ]
+ },
+ {
+ "required": [
+ "service_name",
+ "discovery_type"
+ ]
+ }
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "hash_on": {
+ "type": "string",
+ "default": "vars",
+ "enum": [
+ "vars",
+ "header",
+ "cookie",
+ "consumer",
+ "vars_combinations"
+ ]
+ },
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "upstream_host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "nodes": {
+ "anyOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "host",
+ "weight"
+ ],
+ "properties": {
+ "priority": {
+ "description": "priority of node",
+ "type": "integer",
+ "default": 0
+ },
+ "weight": {
+ "description": "weight of node",
+ "type": "integer",
+ "minimum": 0
+ },
+ "port": {
+ "description": "port of node",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "metadata": {
+ "description": "metadata of node",
+ "type": "object"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "type": {
+ "description": "algorithms of load balancing",
+ "type": "string",
+ "default": "roundrobin"
+ },
+ "discovery_type": {
+ "description": "discovery type",
+ "type": "string"
+ },
+ "discovery_args": {
+ "type": "object",
+ "properties": {
+ "namespace_id": {
+ "description": "namespace id",
+ "type": "string"
+ },
+ "group_name": {
+ "description": "group name",
+ "type": "string"
+ }
+ }
+ },
+ "timeout": {
+ "type": "object",
+ "required": [
+ "connect",
+ "send",
+ "read"
+ ],
+ "properties": {
+ "send": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "connect": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "read": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ }
+ }
+ },
+ "key": {
+ "description": "the key of chash for dynamic load balancing",
+ "type": "string"
+ },
+ "desc": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "labels": {
+ "description": "key/value pairs to specify attributes",
+ "patternProperties": {
+ ".*": {
+ "maxLength": 256,
+ "description": "value of label",
+ "pattern": "^\\S+$",
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "type": "object"
+ },
+ "create_time": {
+ "type": "integer"
+ },
+ "checks": {
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "active"
+ ]
+ },
+ {
+ "required": [
+ "active",
+ "passive"
+ ]
+ }
+ ],
+ "properties": {
+ "active": {
+ "type": "object",
+ "properties": {
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ },
+ "timeouts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 3
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 404,
+ 500,
+ 501,
+ 502,
+ 503,
+ 504,
+ 505
+ ],
+ "uniqueItems": true
+ }
+ }
+ },
+ "port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "req_headers": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "uniqueItems": true
+ },
+ "minItems": 1
+ },
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "timeout": {
+ "type": "number",
+ "default": 1
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "interval": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 302
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "host": {
+ "type": "string",
+ "pattern": "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$"
+ },
+ "concurrency": {
+ "type": "integer",
+ "default": 10
+ },
+ "http_path": {
+ "type": "string",
+ "default": "/"
+ },
+ "https_verify_certificate": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "passive": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "default": "http",
+ "enum": [
+ "http",
+ "https",
+ "tcp"
+ ]
+ },
+ "unhealthy": {
+ "type": "object",
+ "properties": {
+ "timeouts": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 7
+ },
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 429,
+ 500,
+ 503
+ ],
+ "uniqueItems": true
+ },
+ "http_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ },
+ "tcp_failures": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 2
+ }
+ }
+ },
+ "healthy": {
+ "type": "object",
+ "properties": {
+ "http_statuses": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 200,
+ "maximum": 599
+ },
+ "minItems": 1,
+ "default": [
+ 200,
+ 201,
+ 202,
+ 203,
+ 204,
+ 205,
+ 206,
+ 207,
+ 208,
+ 226,
+ 300,
+ 301,
+ 302,
+ 303,
+ 304,
+ 305,
+ 306,
+ 307,
+ 308
+ ],
+ "uniqueItems": true
+ },
+ "successes": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 254,
+ "default": 5
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "retry_timeout": {
+ "type": "number",
+ "minimum": 0
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "tls": {
+ "type": "object",
+ "properties": {
+ "client_key": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 64
+ },
+ "client_cert": {
+ "type": "string",
+ "maxLength": 65536,
+ "minLength": 128
+ },
+ "client_cert_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-_.]+$",
+ "maxLength": 64,
+ "minLength": 1
+ },
+ {
+ "type": "integer",
+ "minimum": 1
+ }
+ ]
+ },
+ "verify": {
+ "type": "boolean",
+ "description": "Turn on server certificate verification, currently
only kafka upstream is supported",
+ "default": false
+ }
+ },
+ "dependencies": {
+ "client_key": {
+ "required": [
+ "client_cert"
+ ]
+ },
+ "client_cert": {
+ "required": [
+ "client_key"
+ ]
+ },
+ "client_cert_id": {
+ "not": {
+ "required": [
+ "client_cert",
+ "client_key"
+ ]
+ }
+ }
+ }
+ },
+ "retries": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "keepalive_pool": {
+ "type": "object",
+ "properties": {
+ "idle_timeout": {
+ "type": "number",
+ "default": 60,
+ "minimum": 0
+ },
+ "requests": {
+ "type": "integer",
+ "default": 1000,
+ "minimum": 1
+ },
+ "size": {
+ "type": "integer",
+ "default": 320,
+ "minimum": 1
+ }
+ }
+ },
+ "service_name": {
+ "type": "string",
+ "maxLength": 256,
+ "minLength": 1
+ },
+ "scheme": {
+ "default": "http",
+ "description": "The scheme of the upstream. For L7 proxy, it can be one
of grpc/grpcs/http/https. For L4 proxy, it can be one of tcp/tls/udp. For
specific protocols, it can be kafka.",
+ "enum": [
+ "grpc",
+ "grpcs",
+ "http",
+ "https",
+ "tcp",
+ "tls",
+ "udp",
+ "kafka"
+ ]
+ },
+ "pass_host": {
+ "description": "mod of host passing",
+ "default": "pass",
+ "type": "string",
+ "enum": [
+ "pass",
+ "node",
+ "rewrite"
+ ]
+ },
+ "update_time": {
+ "type": "integer"
+ }
+ }
+}
diff --git a/src/types/schema/apisix/gateway-contract.test.ts
b/src/types/schema/apisix/gateway-contract.test.ts
new file mode 100644
index 000000000..d38795bf2
--- /dev/null
+++ b/src/types/schema/apisix/gateway-contract.test.ts
@@ -0,0 +1,171 @@
+/**
+ * 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 { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+
+import { describe, expect, it } from 'vitest';
+import type { ZodObject, ZodRawShape, ZodTypeAny } from 'zod';
+
+import { APISIX } from '.';
+
+// Cheapest structural guard for the hand-written zod transcription of the
+// Admin API (#3417): the zod layer must be *looser or equal* to the gateway's
+// own schema, i.e. it must not reject what the gateway accepts. That whole
+// class of "form rejects a valid config" bugs (#3146/#3147/#3362/#3376/#3395)
+// comes from a field the gateway has that the zod schema silently drops.
+//
+// The gateway's per-resource schema is snapshotted under __fixtures__/gateway
+// (refresh with `pnpm refresh:gateway-schema` on APISIX version bumps). For
+// each top-level property the gateway defines this asserts, against the merged
+// dashboard schema:
+// - field coverage: the zod schema knows the field (else the resolver strips
+// it on submit);
+// - enum: the zod field accepts every value the gateway's enum allows.
+//
+// (Required-ness is intentionally NOT compared: the dashboard's full read
+// schema requires the server-managed id/create_time/update_time that the
+// gateway write schema does not, and the write path uses Post/Put variants
+// that omit them, so a required-direction check against the full schema only
+// produces false positives.)
+//
+// Fields the dashboard deliberately does not accept live in ALLOWLIST with a
+// reason — never a silent omission. A stale allowlist entry (one zod now
+// covers) also fails, forcing cleanup. Scope is top-level only; nested objects
+// and plugin configs (passthrough) are out of scope.
+
+const FIXTURE_DIR = fileURLToPath(new URL('./__fixtures__/gateway/',
import.meta.url));
+
+type GatewaySchema = {
+ properties?: Record<string, { enum?: unknown[]; type?: string }>;
+ required?: string[];
+};
+
+const loadFixture = (resource: string): GatewaySchema =>
+ JSON.parse(readFileSync(`${FIXTURE_DIR}${resource}.json`, 'utf8'));
+
+// gateway resource name -> dashboard zod schema (the full stored shape).
+const SCHEMAS: Record<string, ZodTypeAny> = {
+ route: APISIX.Route,
+ service: APISIX.Service,
+ upstream: APISIX.Upstream,
+ consumer: APISIX.Consumer,
+ ssl: APISIX.SSL,
+ global_rule: APISIX.GlobalRule,
+ plugin_config: APISIX.PluginConfig,
+ consumer_group: APISIX.ConsumerGroup,
+ stream_route: APISIX.StreamRoute,
+ proto: APISIX.Proto,
+ credential: APISIX.Credential,
+};
+
+// Gateway top-level fields the dashboard's zod schema does not model. Every
+// entry carries a reason: either an intentional non-support decision, or a
+// KNOWN GAP flagged for a follow-up fix — never a silent omission.
+const ALLOWLIST: Record<string, Record<string, string>> = {
+ route: {},
+ service: {},
+ upstream: {
+ create_time: 'server-managed timestamp not modelled on the upstream schema
(write path omits it; read-only display only)',
+ update_time: 'server-managed timestamp not modelled on the upstream schema
(write path omits it; read-only display only)',
+ },
+ consumer: {
+ id: 'consumers are keyed by username in the dashboard; the gateway id is
not modelled',
+ },
+ ssl: {},
+ global_rule: {},
+ plugin_config: {},
+ consumer_group: {
+ name: 'the dashboard consumer-group form exposes id/desc/labels/plugins,
not a separate name field',
+ },
+ stream_route: {
+ name: 'the base StreamRoute schema omits name (and status); name IS
accepted on the write path via StreamRoutePostSchema (#3437) — the read schema
just does not model it',
+ },
+ proto: {
+ name: 'the dashboard proto form models content/id only',
+ labels: 'the dashboard proto form models content/id only',
+ desc: 'the dashboard proto form models content/id only',
+ },
+ credential: {
+ name: 'credentials are keyed by id in the dashboard; the gateway name is
not modelled',
+ },
+};
+
+// Enum VALUES a covered field is allowed not to accept, as `field="value"`.
+// Same rule: each carries a reason (intentional or a flagged gap).
+const ENUM_ALLOWLIST: Record<string, Record<string, string>> = {
+ upstream: {
+ 'scheme="kafka"':
+ 'KNOWN GAP — the dashboard upstream `scheme` enum omits "kafka"; a
kafka-scheme upstream cannot be created via the form. Flagged for a follow-up
fix (also needs UI support), tracked separately.',
+ },
+};
+
+/** Unwrap ZodEffects/optional/default wrappers down to the underlying object.
*/
+const toObject = (schema: ZodTypeAny): ZodObject<ZodRawShape> => {
+ let cur: ZodTypeAny = schema;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const def = () => (cur as any)._def;
+ while (
+ def()?.typeName === 'ZodEffects' ||
+ def()?.typeName === 'ZodOptional' ||
+ def()?.typeName === 'ZodDefault'
+ ) {
+ cur = def().typeName === 'ZodEffects' ? def().schema : def().innerType;
+ }
+ return cur as ZodObject<ZodRawShape>;
+};
+
+describe('gateway contract: zod is looser-or-equal to the APISIX schema', ()
=> {
+ it.each(Object.keys(SCHEMAS))('%s', (resource) => {
+ const gateway = loadFixture(resource);
+ const properties = gateway.properties ?? {};
+ const shape = toObject(SCHEMAS[resource]).shape;
+ const zodKeys = new Set(Object.keys(shape));
+ const allow = ALLOWLIST[resource] ?? {};
+ const enumAllow = ENUM_ALLOWLIST[resource] ?? {};
+
+ const notCovered: string[] = [];
+ const enumGaps: string[] = [];
+
+ for (const [field, spec] of Object.entries(properties)) {
+ if (!zodKeys.has(field)) {
+ if (!(field in allow)) notCovered.push(field);
+ continue;
+ }
+ const fieldSchema = shape[field] as ZodTypeAny;
+ // enum: every gateway-allowed value must pass the zod field.
+ if (Array.isArray(spec.enum)) {
+ for (const v of spec.enum) {
+ const key = `${field}=${JSON.stringify(v)}`;
+ if (!fieldSchema.safeParse(v).success && !(key in enumAllow)) {
+ enumGaps.push(key);
+ }
+ }
+ }
+ }
+
+ // Stale allowlist: a field-coverage exemption the zod schema now covers,
+ // or an exemption for a field the gateway no longer defines.
+ const staleAllow = Object.keys(allow).filter(
+ (f) => zodKeys.has(f) || !(f in properties)
+ );
+
+ expect(
+ { notCovered, enumGaps, staleAllow },
+ `${resource}: zod is stricter than the gateway (or the allowlist is
stale)`
+ ).toEqual({ notCovered: [], enumGaps: [], staleAllow: [] });
+ });
+});