diff --git a/apps/cli/src/command/lint.command.ts b/apps/cli/src/command/lint.command.ts index 5249478e..6d47b891 100644 --- a/apps/cli/src/command/lint.command.ts +++ b/apps/cli/src/command/lint.command.ts @@ -4,6 +4,7 @@ import pluralize from 'pluralize'; import { ZodError } from 'zod'; import { check } from '../linter'; +import { SignaleRenderer } from '../utils/listr'; import { LoadLocalConfigurationTask } from './diff.command'; import { BaseCommand } from './helper'; @@ -28,7 +29,15 @@ export const LintTask = (): ListrTask<{ local: ADCSDK.Configuration }> => ({ // normal case const resourceType = pluralize.singular(error.path[0] as string); - const resourceName = ctx.local[error.path[0]][error.path[1]].name; + const resource = ctx.local[error.path[0]][error.path[1]]; + const resourceName = + resourceType === 'global_rule' || resourceType === 'plugin_metadata' + ? error.path[1] + : resourceType === 'ssl' + ? resource.snis + : resourceType === 'consumer' + ? resource.username + : resource.name; err += `#${idx + 1} ${ error.message } at ${resourceType}: "${resourceName}", field: "${( @@ -59,10 +68,13 @@ export const LintCommand = new BaseCommand('lint') .action(async () => { const opts = LintCommand.optsWithGlobals(); - const tasks = new Listr([ - LoadLocalConfigurationTask(opts.file, {}), - LintTask(), - ]); + const tasks = new Listr( + [LoadLocalConfigurationTask(opts.file, {}), LintTask()], + { + renderer: SignaleRenderer, + rendererOptions: { verbose: opts.verbose }, + }, + ); try { await tasks.run(); diff --git a/apps/cli/src/linter/exporter.ts b/apps/cli/src/linter/exporter.ts new file mode 100644 index 00000000..5fe265fa --- /dev/null +++ b/apps/cli/src/linter/exporter.ts @@ -0,0 +1,15 @@ +/** + * Export jsonschema file by: + * + * $ ts-node apps/cli/src/linter/exporter.ts + * + */ +import { writeFileSync } from 'fs'; +import zodToJsonSchema from 'zod-to-json-schema'; + +import { ConfigurationSchema } from './schema'; + +writeFileSync( + 'schema.json', + JSON.stringify(zodToJsonSchema(ConfigurationSchema), null, 2), +); diff --git a/apps/cli/src/linter/schema.ts b/apps/cli/src/linter/schema.ts index e777b939..56ff3f82 100644 --- a/apps/cli/src/linter/schema.ts +++ b/apps/cli/src/linter/schema.ts @@ -1,3 +1,4 @@ +import { max } from 'lodash'; import { z } from 'zod'; const nameSchema = z.string().min(1).max(100); @@ -11,15 +12,19 @@ const exprSchema = z.array( z.union([z.string(), z.array(z.lazy(() => exprSchema))]), ); const timeoutSchema = z.object({ - connect: z.number(), - send: z.number(), - read: z.number(), + connect: z.number().gt(0), + send: z.number().gt(0), + read: z.number().gt(0), }); const portSchema = z.number().int().min(1).max(65535); const certificateSchema = z .string() .min(128) .max(64 * 1024); +const certificateKeySchema = z + .string() + .min(32) + .max(64 * 1024); const upstreamHealthCheckPassiveHealthy = z .object({ @@ -36,7 +41,7 @@ const upstreamHealthCheckPassiveUnhealthy = z http_statuses: z .array(z.number().int().min(200).max(599)) .min(1) - .default([200, 302]) + .default([429, 404, 500, 501, 502, 503, 504, 505]) .optional(), http_failures: z.number().int().min(1).max(254).default(5).optional(), tcp_failures: z.number().int().min(1).max(254).default(2).optional(), @@ -46,242 +51,258 @@ const upstreamHealthCheckPassiveUnhealthy = z const upstreamHealthCheckType = z .enum(['http', 'https', 'tcp']) .default('http'); -const upstreamSchema = z.object({ - name: nameSchema, - description: descriptionSchema.optional(), - labels: labelsSchema.optional(), - - type: z.enum(['roundrobin', 'chash', 'least_conn', 'ewma']).optional(), - hash_on: z.string().optional(), - key: z.string().optional(), - checks: z - .object({ - active: z - .object({ - type: upstreamHealthCheckType.optional(), - timeout: z.number().default(1).optional(), - concurrency: z.number().default(10).optional(), - host: z.string(), //TODO - port: portSchema, - http_path: z.string().default('/').optional(), - https_verify_cert: z.boolean().default(true).optional(), - http_request_headers: z.array(z.string()).min(1).optional(), - healthy: z - .object({ - interval: z.number().int().min(1).default(1), - }) - .merge(upstreamHealthCheckPassiveHealthy) - .strict() - .optional(), - unhealthy: z - .object({ - interval: z.number().int().min(1).default(1), - }) - .merge(upstreamHealthCheckPassiveUnhealthy) - .strict() - .optional(), - }) - .optional(), - passive: z - .object({ - type: upstreamHealthCheckType.optional(), - healthy: upstreamHealthCheckPassiveHealthy.optional(), - unhealthy: upstreamHealthCheckPassiveUnhealthy.optional(), - }) - .optional(), - }) - .refine( - (data) => (data.active && data.passive) || (data.active && !data.passive), - ) - .optional(), - nodes: z.array( - z.object({ - host: z.string(), - port: portSchema.optional(), - weight: z.number().int().min(0), - priority: z.number().default(0).optional(), - metadata: z.record(z.string(), z.any()).optional(), - }), - ), - scheme: z - .enum(['grpc', 'grpcs', 'http', 'https', 'tcp', 'tls', 'udp', 'kafka']) - .default('http') - .optional(), - retries: z.number().int().min(0).optional(), - retry_timeout: z.number().min(0).optional(), - timeout: timeoutSchema.optional(), - tls: z - .object({ - cert: z.string(), - key: z.string(), - client_cert_id: z.string(), - verify: z.boolean(), - }) - .refine( - (data) => - (data.cert && data.key && !data.client_cert_id) || - (data.client_cert_id && !data.cert && !data.key), - ) - .optional(), - keepalive_pool: z - .object({ - size: z.number().int().min(1).default(320), - idle_timeout: z.number().min(0).default(60), - requests: z.number().int().min(1).default(1000), - }) - .optional(), - pass_host: z.enum(['pass', 'node', 'rewrite']).default('pass').optional(), - upstream_host: z.string().optional(), +const upstreamSchema = z + .object({ + name: nameSchema.optional(), + description: descriptionSchema.optional(), + labels: labelsSchema.optional(), - service_name: z.string().optional(), - discovery_type: z.string().optional(), - discovery_args: z.record(z.string(), z.any()).optional(), -}); + type: z + .enum(['roundrobin', 'chash', 'least_conn', 'ewma']) + .default('roundrobin') + .optional(), + hash_on: z.string().optional(), + key: z.string().optional(), + checks: z + .object({ + active: z + .object({ + type: upstreamHealthCheckType.optional(), + timeout: z.number().default(1).optional(), + concurrency: z.number().default(10).optional(), + host: z.string(), + port: portSchema, + http_path: z.string().default('/').optional(), + https_verify_cert: z.boolean().default(true).optional(), + http_request_headers: z.array(z.string()).min(1).optional(), + healthy: z + .object({ + interval: z.number().int().min(1).default(1), + }) + .merge(upstreamHealthCheckPassiveHealthy) + .strict() + .optional(), + unhealthy: z + .object({ + interval: z.number().int().min(1).default(1), + }) + .merge(upstreamHealthCheckPassiveUnhealthy) + .strict() + .optional(), + }) + .optional(), + passive: z + .object({ + type: upstreamHealthCheckType.optional(), + healthy: upstreamHealthCheckPassiveHealthy.optional(), + unhealthy: upstreamHealthCheckPassiveUnhealthy.optional(), + }) + .optional(), + }) + .refine( + (data) => + (data.active && data.passive) || (data.active && !data.passive), + { + message: + 'Passive health checks must be enabled at the same time as active health checks', + }, + ) + .optional(), + nodes: z.array( + z.object({ + host: z.string(), + port: portSchema.optional(), + weight: z.number().int().min(0), + priority: z.number().default(0).optional(), + metadata: z.record(z.string(), z.any()).optional(), + }), + ), + scheme: z + .enum(['grpc', 'grpcs', 'http', 'https', 'tcp', 'tls', 'udp', 'kafka']) + .default('http') + .optional(), + retries: z.number().int().min(0).max(65535).optional(), + retry_timeout: z.number().min(0).optional(), + timeout: timeoutSchema.optional(), + tls: z + .object({ + cert: z.string(), + key: z.string(), + client_cert_id: z.string(), + verify: z.boolean(), + }) + .refine( + (data) => + (data.cert && data.key && !data.client_cert_id) || + (data.client_cert_id && !data.cert && !data.key), + ) + .optional(), + keepalive_pool: z + .object({ + size: z.number().int().min(1).default(320), + idle_timeout: z.number().min(0).default(60), + requests: z.number().int().min(1).default(1000), + }) + .optional(), + pass_host: z.enum(['pass', 'node', 'rewrite']).default('pass').optional(), + upstream_host: z.string().optional(), -const refineUpstream = (obj: z.ZodObject) => { - return obj.refine( - (data) => - (data.nodes && !data.discovery_type && !data.service_name) || - (data.discovery_type && data.service_name && !data.nodes), + service_name: z.string().optional(), + discovery_type: z.string().optional(), + discovery_args: z.record(z.string(), z.any()).optional(), + }) + .strict() + .refine( + (val) => + (val.nodes && !val.discovery_type && !val.service_name) || + (val.discovery_type && val.service_name && !val.nodes), + { + message: + 'Upstream must either explicitly specify nodes or use service discovery and not both', + }, ); -}; -const routeSchema = z.object({ - name: nameSchema, - description: descriptionSchema.optional(), - labels: labelsSchema.optional(), +const routeSchema = z + .object({ + name: nameSchema, + description: descriptionSchema.optional(), + labels: labelsSchema.optional(), - hosts: z.array(z.string()).optional(), - uris: z.array(z.string()).min(1), - priority: z.number().int().optional(), - timeout: timeoutSchema.optional(), - vars: exprSchema.optional(), - methods: z - .array( - z.enum([ - 'GET', - 'POST', - 'PUT', - 'DELETE', - 'PATCH', - 'HEAD', - 'OPTIONS', - 'CONNECT', - 'TRACE', - 'PURGE', - ]), - ) - .optional(), - enable_websocket: z.boolean().optional(), - remote_addrs: z.array(z.string().ip()).optional(), - plugins: pluginsSchema.optional(), + hosts: z.array(z.string()).optional(), + uris: z.array(z.string()).min(1), + priority: z.number().int().optional(), + timeout: timeoutSchema.optional(), + vars: exprSchema.optional(), + methods: z + .array( + z.enum([ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + 'HEAD', + 'OPTIONS', + 'CONNECT', + 'TRACE', + 'PURGE', + ]), + ) + .nonempty() + .optional(), + enable_websocket: z.boolean().optional(), + remote_addrs: z.array(z.string().ip()).optional(), + plugins: pluginsSchema.optional(), + filter_func: z.string().optional(), + }) + .strict(); - plugin_config_id: z.string().optional(), - filter_func: z.string().optional(), -}); +const streamRouteSchema = z + .object({ + name: nameSchema, + description: descriptionSchema.optional(), + labels: labelsSchema.optional(), -const streamRouteSchema = z.object({ - name: nameSchema, - description: descriptionSchema.optional(), - labels: labelsSchema.optional(), + plugins: pluginsSchema.optional(), - remote_addr: z.string().optional(), - server_addr: z.string().optional(), - server_port: portSchema.optional(), - sni: z.string().optional(), -}); + remote_addr: z.string().optional(), + server_addr: z.string().optional(), + server_port: portSchema.optional(), + sni: z.string().optional(), + }) + .strict(); -const serviceSchema = z.object({ - name: nameSchema, - description: descriptionSchema.optional(), - labels: labelsSchema.optional(), +const serviceSchema = z + .object({ + name: nameSchema, + description: descriptionSchema.optional(), + labels: labelsSchema.optional(), - upstream: refineUpstream( - upstreamSchema.extend({ name: nameSchema.optional() }), - ).optional(), - plugins: pluginsSchema.optional(), + upstream: upstreamSchema.optional(), + plugins: pluginsSchema.optional(), + path_prefix: z + .string() + .optional() + .refine((val) => val?.startsWith('/'), { + message: 'Path prefix must start with "/"', + }), + strip_path_prefix: z.boolean().optional(), + hosts: z.array(z.string()).optional(), - routes: z.array(routeSchema).optional(), - stream_routes: z.array(streamRouteSchema).optional(), -}); + routes: z.array(routeSchema).optional(), + stream_routes: z.array(streamRouteSchema).optional(), + }) + .strict() + .refine( + (val) => !(Array.isArray(val.routes) && Array.isArray(val.stream_routes)), + { + message: + 'HTTP routes and Stream routes are mutually exclusive and should not exist in the same service', + }, + ); -const sslSchema = z.object({ - labels: labelsSchema.optional(), +const sslSchema = z + .object({ + labels: labelsSchema.optional(), - type: z.enum(['server', 'client']).default('server').optional(), - snis: z.array(z.string().min(1)), - certificates: z.array( - z + type: z.enum(['server', 'client']).default('server').optional(), + snis: z.array(z.string().min(1)).min(1), + certificates: z + .array( + z + .object({ + certificate: certificateSchema, + key: certificateKeySchema, + }) + .strict(), + ) + .refine((val) => val.length > 0, { + message: 'SSL must contain at least one certificate', + }), + client: z .object({ - certificate: certificateSchema, - key: certificateSchema, + ca: certificateSchema, + depth: z.number().int().min(0).default(1).optional(), + skip_mtls_uri_regex: z.array(z.string()).min(1).optional(), }) - .strict(), - ), - client: z - .object({ - ca: certificateSchema, - depth: z.number().int().min(0).default(1).optional(), - skip_mtls_uri_regex: z.array(z.string()).min(1).optional(), - }) - .strict(), - ssl_protocols: z.array(z.enum(['TLSv1.1', 'TLSv1.2', 'TLSv1.3'])).max(3), -}); - -const pluginConfigSchema = z.object({ - name: nameSchema, - description: descriptionSchema.optional(), - labels: labelsSchema.optional(), - - plugins: pluginsSchema.optional(), -}); - -const consumerSchema = z.object({ - username: z.string().min(1), - description: descriptionSchema.optional(), - labels: labelsSchema.optional(), - - plugins: pluginsSchema.optional(), -}); + .strict() + .optional(), + ssl_protocols: z + .array(z.enum(['TLSv1.1', 'TLSv1.2', 'TLSv1.3'])) + .nonempty() + .optional(), + }) + .strict(); -const consumerGroupSchema = z.object({ - name: nameSchema, - description: descriptionSchema.optional(), - labels: labelsSchema.optional(), +const consumerSchema = z + .object({ + username: nameSchema, + description: descriptionSchema.optional(), + labels: labelsSchema.optional(), - plugins: pluginsSchema, + plugins: pluginsSchema.optional(), + }) + .strict(); - consumers: z.array(consumerSchema).optional(), -}); +const consumerGroupSchema = z + .object({ + name: nameSchema, + description: descriptionSchema.optional(), + labels: labelsSchema.optional(), -export const ConfigurationSchema = z.object({ - routes: z.array(routeSchema).optional(), - services: z.array(serviceSchema).optional(), - upstreams: z.array(refineUpstream(upstreamSchema)).optional(), - ssls: z.array(sslSchema).optional(), - plugin_configs: z.array(pluginConfigSchema).optional(), - consumers: z.array(consumerSchema).optional(), - consumer_groups: z.array(consumerGroupSchema).optional(), - stream_routes: z.array(streamRouteSchema).optional(), - global_rules: z.record(z.string(), z.record(z.string(), z.any())).optional(), - plugin_metadata: z - .record(z.string(), z.record(z.string(), z.any())) - .optional(), -}); + plugins: pluginsSchema, -/* const res = configurationSchema.safeParse({ - services: [ - { - name: 'str', - description: '', - labels: { - ADC_TEST: '123', - ADC_TSET: ['123', '234'], - }, - }, - ], -} as z.infer); + consumers: z.array(consumerSchema).optional(), + }) + .strict(); -if (!res.success) { - console.log((res as SafeParseError).error); -} */ +export const ConfigurationSchema = z + .object({ + services: z.array(serviceSchema).optional(), + ssls: z.array(sslSchema).optional(), + consumers: z.array(consumerSchema).optional(), + consumer_groups: z.array(consumerGroupSchema).optional(), + global_rules: pluginsSchema.optional(), + plugin_metadata: pluginsSchema.optional(), + }) + .strict(); diff --git a/libs/sdk/src/core/index.ts b/libs/sdk/src/core/index.ts index 93282c63..ac09d615 100644 --- a/libs/sdk/src/core/index.ts +++ b/libs/sdk/src/core/index.ts @@ -20,7 +20,6 @@ export interface Route { enable_websocket?: boolean; remote_addrs?: Array; plugins?: Plugins; - plugin_config_id?: string; filter_func?: string; service_id?: string; @@ -83,7 +82,7 @@ export interface UpstreamHealthCheck { passive: UpstreamHealthCheckPassive; } export interface UpstreamHealthCheckActive { - type?: string; + type?: 'http' | 'https' | 'tcp'; timeout?: number; concurrency?: number; host: string; diff --git a/schema.json b/schema.json new file mode 100644 index 00000000..105a789f --- /dev/null +++ b/schema.json @@ -0,0 +1,702 @@ +{ + "type": "object", + "properties": { + "services": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 256 + }, + "labels": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "upstream": { + "type": "object", + "properties": { + "name": { + "$ref": "#/properties/services/items/properties/name" + }, + "description": { + "$ref": "#/properties/services/items/properties/description" + }, + "labels": { + "$ref": "#/properties/services/items/properties/labels" + }, + "type": { + "type": "string", + "enum": [ + "roundrobin", + "chash", + "least_conn", + "ewma" + ], + "default": "roundrobin" + }, + "hash_on": { + "type": "string" + }, + "key": { + "type": "string" + }, + "checks": { + "type": "object", + "properties": { + "active": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http", + "https", + "tcp" + ], + "default": "http" + }, + "timeout": { + "type": "number", + "default": 1 + }, + "concurrency": { + "type": "number", + "default": 10 + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "http_path": { + "type": "string", + "default": "/" + }, + "https_verify_cert": { + "type": "boolean", + "default": true + }, + "http_request_headers": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 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 + ] + }, + "successes": { + "type": "integer", + "minimum": 1, + "maximum": 254, + "default": 2 + } + }, + "additionalProperties": false + }, + "unhealthy": { + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "http_statuses": { + "type": "array", + "items": { + "type": "integer", + "minimum": 200, + "maximum": 599 + }, + "minItems": 1, + "default": [ + 429, + 404, + 500, + 501, + 502, + 503, + 504, + 505 + ] + }, + "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 + } + }, + "additionalProperties": false + } + }, + "required": [ + "host", + "port" + ], + "additionalProperties": false + }, + "passive": { + "type": "object", + "properties": { + "type": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/type" + }, + "healthy": { + "type": "object", + "properties": { + "http_statuses": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/healthy/properties/http_statuses" + }, + "successes": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/healthy/properties/successes" + } + }, + "additionalProperties": false + }, + "unhealthy": { + "type": "object", + "properties": { + "http_statuses": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/unhealthy/properties/http_statuses" + }, + "http_failures": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/unhealthy/properties/http_failures" + }, + "tcp_failures": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/unhealthy/properties/tcp_failures" + }, + "timeouts": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/unhealthy/properties/timeouts" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "nodes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "port": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/port" + }, + "weight": { + "type": "integer", + "minimum": 0 + }, + "priority": { + "type": "number", + "default": 0 + }, + "metadata": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "host", + "weight" + ], + "additionalProperties": false + } + }, + "scheme": { + "type": "string", + "enum": [ + "grpc", + "grpcs", + "http", + "https", + "tcp", + "tls", + "udp", + "kafka" + ], + "default": "http" + }, + "retries": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "retry_timeout": { + "type": "number", + "minimum": 0 + }, + "timeout": { + "type": "object", + "properties": { + "connect": { + "type": "number", + "exclusiveMinimum": 0 + }, + "send": { + "type": "number", + "exclusiveMinimum": 0 + }, + "read": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "required": [ + "connect", + "send", + "read" + ], + "additionalProperties": false + }, + "tls": { + "type": "object", + "properties": { + "cert": { + "type": "string" + }, + "key": { + "type": "string" + }, + "client_cert_id": { + "type": "string" + }, + "verify": { + "type": "boolean" + } + }, + "required": [ + "cert", + "key", + "client_cert_id", + "verify" + ], + "additionalProperties": false + }, + "keepalive_pool": { + "type": "object", + "properties": { + "size": { + "type": "integer", + "minimum": 1, + "default": 320 + }, + "idle_timeout": { + "type": "number", + "minimum": 0, + "default": 60 + }, + "requests": { + "type": "integer", + "minimum": 1, + "default": 1000 + } + }, + "additionalProperties": false + }, + "pass_host": { + "type": "string", + "enum": [ + "pass", + "node", + "rewrite" + ], + "default": "pass" + }, + "upstream_host": { + "type": "string" + }, + "service_name": { + "type": "string" + }, + "discovery_type": { + "type": "string" + }, + "discovery_args": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "nodes" + ], + "additionalProperties": false + }, + "plugins": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "path_prefix": { + "type": "string" + }, + "strip_path_prefix": { + "type": "boolean" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "routes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "$ref": "#/properties/services/items/properties/name" + }, + "description": { + "$ref": "#/properties/services/items/properties/description" + }, + "labels": { + "$ref": "#/properties/services/items/properties/labels" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "uris": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "priority": { + "type": "integer" + }, + "timeout": { + "$ref": "#/properties/services/items/properties/upstream/properties/timeout" + }, + "vars": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/properties/services/items/properties/routes/items/properties/vars" + } + } + ] + } + }, + "methods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + "CONNECT", + "TRACE", + "PURGE" + ] + }, + "minItems": 1 + }, + "enable_websocket": { + "type": "boolean" + }, + "remote_addrs": { + "type": "array", + "items": { + "type": "string", + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + } + }, + "plugins": { + "$ref": "#/properties/services/items/properties/plugins" + }, + "filter_func": { + "type": "string" + } + }, + "required": [ + "name", + "uris" + ], + "additionalProperties": false + } + }, + "stream_routes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "$ref": "#/properties/services/items/properties/name" + }, + "description": { + "$ref": "#/properties/services/items/properties/description" + }, + "labels": { + "$ref": "#/properties/services/items/properties/labels" + }, + "plugins": { + "$ref": "#/properties/services/items/properties/plugins" + }, + "remote_addr": { + "type": "string" + }, + "server_addr": { + "type": "string" + }, + "server_port": { + "$ref": "#/properties/services/items/properties/upstream/properties/checks/properties/active/properties/port" + }, + "sni": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + } + }, + "required": [ + "name", + "path_prefix" + ], + "additionalProperties": false + } + }, + "ssls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "labels": { + "$ref": "#/properties/services/items/properties/labels" + }, + "type": { + "type": "string", + "enum": [ + "server", + "client" + ], + "default": "server" + }, + "snis": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, + "certificates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "certificate": { + "type": "string", + "minLength": 128, + "maxLength": 65536 + }, + "key": { + "type": "string", + "minLength": 32, + "maxLength": 65536 + } + }, + "required": [ + "certificate", + "key" + ], + "additionalProperties": false + } + }, + "client": { + "type": "object", + "properties": { + "ca": { + "$ref": "#/properties/ssls/items/properties/certificates/items/properties/certificate" + }, + "depth": { + "type": "integer", + "minimum": 0, + "default": 1 + }, + "skip_mtls_uri_regex": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "ca" + ], + "additionalProperties": false + }, + "ssl_protocols": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "TLSv1.1", + "TLSv1.2", + "TLSv1.3" + ] + }, + "minItems": 1 + } + }, + "required": [ + "snis", + "certificates" + ], + "additionalProperties": false + } + }, + "consumers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "username": { + "$ref": "#/properties/services/items/properties/name" + }, + "description": { + "$ref": "#/properties/services/items/properties/description" + }, + "labels": { + "$ref": "#/properties/services/items/properties/labels" + }, + "plugins": { + "$ref": "#/properties/services/items/properties/plugins" + } + }, + "required": [ + "username" + ], + "additionalProperties": false + } + }, + "consumer_groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "$ref": "#/properties/services/items/properties/name" + }, + "description": { + "$ref": "#/properties/services/items/properties/description" + }, + "labels": { + "$ref": "#/properties/services/items/properties/labels" + }, + "plugins": { + "$ref": "#/properties/services/items/properties/plugins" + }, + "consumers": { + "type": "array", + "items": { + "$ref": "#/properties/consumers/items" + } + } + }, + "required": [ + "name", + "plugins" + ], + "additionalProperties": false + } + }, + "global_rules": { + "$ref": "#/properties/services/items/properties/plugins" + }, + "plugin_metadata": { + "$ref": "#/properties/services/items/properties/plugins" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file