From 68057ccaed6cc3559bf1cd0e1dff74a22540d675 Mon Sep 17 00:00:00 2001 From: Gabriele Gerbino Date: Mon, 24 Oct 2022 11:46:31 +0200 Subject: [PATCH] feat: support JSON schemas in the FillEntityDefaults function FillEntityDefaults can be used to inject Kong entity defaults into a Kong entity struct. This function achieves this by getting entity schemas from the Admin API, flattening the returned schemas and merging these with the input schemas. Kong CP only works with Lua schemas, while Konnect also supports JSON schemas. This commit adds support to filling entity defaults from JSON schemas. --- CHANGELOG.md | 9 + kong/testdata/routeJSONSchema.json | 752 ++++++++++++++++++++ kong/testdata/serviceJSONSchema.json | 302 ++++++++ kong/testdata/targetJSONSchema.json | 61 ++ kong/testdata/upstreamJSONSchema.json | 948 ++++++++++++++++++++++++++ kong/utils.go | 74 +- kong/utils_test.go | 360 ++++++++++ 7 files changed, 2504 insertions(+), 2 deletions(-) create mode 100644 kong/testdata/routeJSONSchema.json create mode 100644 kong/testdata/serviceJSONSchema.json create mode 100644 kong/testdata/targetJSONSchema.json create mode 100644 kong/testdata/upstreamJSONSchema.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1574cbbd1..b52fbef48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Table of Contents +- [Unreleased](#Unreleased) - [v0.33.0](#v0330) - [v0.32.0](#v0320) - [v0.31.1](#v0311) @@ -41,6 +42,13 @@ - [0.2.0](#020) - [0.1.0](#010) +## [Unreleased] + +> Release date: TBD + +- Add support to Kong Vaults + [#224](https://github.com/Kong/go-kong/pull/224) + ## [v0.33.0] > Release date: 2022/10/05 @@ -619,6 +627,7 @@ authentication credentials in Kong. releases of Kong since every release of Kong is introducing breaking changes to the Admin API. +[Unreleased]: https://github.com/Kong/go-kong/compare/v0.33.0...Unreleased [v0.33.0]: https://github.com/Kong/go-kong/compare/v0.32.0...v0.33.0 [v0.32.0]: https://github.com/Kong/go-kong/compare/v0.31.1...v0.32.0 [v0.31.1]: https://github.com/Kong/go-kong/compare/v0.31.0...v0.31.1 diff --git a/kong/testdata/routeJSONSchema.json b/kong/testdata/routeJSONSchema.json new file mode 100644 index 000000000..1d7671337 --- /dev/null +++ b/kong/testdata/routeJSONSchema.json @@ -0,0 +1,752 @@ +{ + "additionalProperties": false, + "allOf": [ + { + "description": "'snis' can be set only when protocols has one of 'https', 'grpcs', 'tls' or 'tls_passthrough'", + "if": { + "required": [ + "snis" + ] + }, + "then": { + "properties": { + "protocols": { + "contains": { + "oneOf": [ + { + "const": "https", + "type": "string" + }, + { + "const": "grpcs", + "type": "string" + }, + { + "const": "tls", + "type": "string" + }, + { + "const": "tls_passthrough", + "type": "string" + } + ] + } + } + } + }, + "title": "sni_rule" + }, + { + "description": "when protocols has 'http' or 'https', 'sources' or 'destinations' cannot be set", + "if": { + "properties": { + "protocols": { + "contains": { + "anyOf": [ + { + "const": "https", + "type": "string" + }, + { + "const": "http", + "type": "string" + } + ] + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "properties": { + "destinations": { + "not": {} + }, + "sources": { + "not": { + "description": "when protocols has 'http' or 'https', 'sources' or 'destination' cannot be set" + } + } + } + } + }, + { + "description": "when protocols has 'http', at least one of 'hosts', 'methods', 'paths' or 'headers' must be set", + "if": { + "properties": { + "protocols": { + "contains": { + "const": "http" + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "methods" + ] + }, + { + "required": [ + "hosts" + ] + }, + { + "required": [ + "paths" + ] + }, + { + "required": [ + "paths" + ] + }, + { + "required": [ + "headers" + ] + } + ] + } + }, + { + "description": "when protocols has 'https', at least one of 'snis', 'hosts', 'methods', 'paths' or 'headers' must be set", + "if": { + "properties": { + "protocols": { + "contains": { + "const": "https" + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "methods" + ] + }, + { + "required": [ + "hosts" + ] + }, + { + "required": [ + "paths" + ] + }, + { + "required": [ + "paths" + ] + }, + { + "required": [ + "headers" + ] + }, + { + "required": [ + "snis" + ] + } + ] + } + }, + { + "description": "when protocol has 'tcp', 'tls', 'tls_passthrough' or 'udp', 'methods', 'hosts', 'paths', 'headers' cannot be set", + "if": { + "properties": { + "protocols": { + "contains": { + "anyOf": [ + { + "const": "tcp", + "type": "string" + }, + { + "const": "udp", + "type": "string" + }, + { + "const": "tls", + "type": "string" + }, + { + "const": "tls_passthrough", + "type": "string" + } + ] + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "properties": { + "headers": { + "not": {} + }, + "hosts": { + "not": {} + }, + "methods": { + "not": {} + }, + "paths": { + "not": {} + } + } + } + }, + { + "description": "when protocols has 'tcp', 'tls' or 'udp', then at least one of 'sources', 'destinations' or 'snis' must be set", + "if": { + "properties": { + "protocols": { + "contains": { + "anyOf": [ + { + "const": "tcp", + "type": "string" + }, + { + "const": "udp", + "type": "string" + }, + { + "const": "tls", + "type": "string" + } + ] + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "sources" + ] + }, + { + "required": [ + "destinations" + ] + }, + { + "required": [ + "snis" + ] + } + ] + } + }, + { + "description": "when protocol has 'grpc' or 'grpcs', 'strip_path', 'methods', 'sources', 'destinations' cannot be set", + "if": { + "properties": { + "protocols": { + "contains": { + "anyOf": [ + { + "const": "grpc", + "type": "string" + }, + { + "const": "grpcs", + "type": "string" + } + ] + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "properties": { + "destinations": { + "not": {} + }, + "methods": { + "not": {} + }, + "sources": { + "not": {} + }, + "strip_path": { + "not": { + "const": true + } + } + } + } + }, + { + "description": "when protocols has 'grpc', at least one of 'hosts', 'headers' or 'paths' must be set", + "if": { + "properties": { + "protocols": { + "contains": { + "const": "grpc" + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "hosts" + ] + }, + { + "required": [ + "headers" + ] + }, + { + "required": [ + "paths" + ] + } + ] + } + }, + { + "description": "when protocols has 'grpcs', at least one of 'hosts', 'headers', 'paths' or 'snis' must be set", + "if": { + "properties": { + "protocols": { + "contains": { + "const": "grpcs" + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "hosts" + ] + }, + { + "required": [ + "headers" + ] + }, + { + "required": [ + "paths" + ] + }, + { + "required": [ + "snis" + ] + } + ] + } + }, + { + "description": "when protocols has 'tls_passthrough', 'snis' must be set", + "if": { + "properties": { + "protocols": { + "contains": { + "const": "tls_passthrough" + } + } + }, + "required": [ + "protocols" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "snis" + ] + } + ] + } + }, + { + "description": "'ws' and 'wss' protocols are Kong Enterprise-only features. Please upgrade to Kong Enterprise to use this feature.", + "not": { + "properties": { + "protocols": { + "contains": { + "anyOf": [ + { + "const": "ws", + "type": "string" + }, + { + "const": "wss", + "type": "string" + } + ] + } + } + }, + "required": [ + "protocols" + ] + }, + "title": "ws_protocols_rule" + } + ], + "properties": { + "created_at": { + "minimum": 1, + "type": "integer" + }, + "destinations": { + "items": { + "anyOf": [ + { + "description": "at least one of 'ip' or 'port' is required", + "required": [ + "ip" + ] + }, + { + "description": "at least one of 'ip' or 'port' is required", + "required": [ + "port" + ] + } + ], + "properties": { + "ip": { + "anyOf": [ + { + "description": "must be a valid IP or CIDR", + "pattern": "^([0-9]{1,3}[.]{1}){3}[0-9]{1,3}$" + }, + { + "description": "must be a valid IP or CIDR", + "pattern": "^([0-9]{1,3}[.]{1}){3}[0-9]{1,3}/[0-9]{1,3}$" + } + ], + "type": "string" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "maxItems": 16, + "type": "array" + }, + "headers": { + "additionalProperties": false, + "maxProperties": 16, + "patternProperties": { + "^[A-Za-z0-9!#$%&'*+-.^_|~]{1,64}$": { + "properties": { + "values": { + "items": { + "maxLength": 64, + "type": "string" + }, + "maxItems": 16, + "type": "array" + } + }, + "type": "object" + }, + "^[Hh][Oo][Ss][Tt]$": { + "not": { + "description": "must not contain 'host' header" + } + } + }, + "type": "object" + }, + "hosts": { + "items": { + "description": "must be a valid hostname", + "maxLength": 256, + "pattern": "^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$", + "type": "string" + }, + "maxItems": 16, + "type": "array" + }, + "https_redirect_status_code": { + "default": 426, + "enum": [ + 426, + 301, + 302, + 307, + 308 + ], + "type": "integer" + }, + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + }, + "methods": { + "items": { + "maxItems": 16, + "pattern": "^[A-Z]+$", + "type": "string" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[0-9a-zA-Z.\\-_~]*$", + "type": "string" + }, + "path_handling": { + "default": "v0", + "enum": [ + "v0", + "v1" + ], + "type": "string" + }, + "paths": { + "items": { + "allOf": [ + { + "description": "must begin with `/` (prefix path) or `~/` (regex path)", + "pattern": "^/.*|^~/.*" + }, + { + "description": "length must not exceed 1024", + "maxLength": 1024 + }, + { + "not": { + "description": "must not contain `//`", + "pattern": "//" + } + } + ], + "type": "string" + }, + "maxItems": 16, + "type": "array" + }, + "preserve_host": { + "default": false, + "type": "boolean" + }, + "protocols": { + "anyOf": [ + { + "description": "must contain only one subset [ http https ]", + "items": { + "enum": [ + "http", + "https" + ], + "type": "string" + } + }, + { + "description": "must contain only one subset [ tcp udp tls ]", + "items": { + "enum": [ + "tcp", + "udp", + "tls" + ], + "type": "string" + } + }, + { + "description": "must contain only one subset [ grpc grpcs ]", + "items": { + "enum": [ + "grpc", + "grpcs" + ], + "type": "string" + } + }, + { + "description": "must contain only one subset [ tls_passthrough ]", + "items": { + "enum": [ + "tls_passthrough" + ], + "type": "string" + } + }, + { + "description": "must contain only one subset [ ws wss ]", + "items": { + "enum": [ + "ws", + "wss" + ], + "type": "string" + } + } + ], + "default": [ + "http", + "https" + ], + "items": { + "enum": [ + "http", + "https", + "grpc", + "grpcs", + "tcp", + "udp", + "tls", + "tls_passthrough", + "ws", + "wss" + ], + "type": "string" + }, + "type": "array" + }, + "regex_priority": { + "default": 0, + "exclusiveMinimum": -1, + "type": "integer" + }, + "request_buffering": { + "default": true, + "type": "boolean" + }, + "response_buffering": { + "default": true, + "type": "boolean" + }, + "service": { + "additionalProperties": false, + "description": "foreign", + "properties": { + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "snis": { + "items": { + "description": "must be a valid hostname", + "maxLength": 256, + "pattern": "^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$", + "type": "string" + }, + "maxItems": 16, + "type": "array" + }, + "sources": { + "items": { + "anyOf": [ + { + "description": "at least one of 'ip' or 'port' is required", + "required": [ + "ip" + ] + }, + { + "description": "at least one of 'ip' or 'port' is required", + "required": [ + "port" + ] + } + ], + "properties": { + "ip": { + "anyOf": [ + { + "description": "must be a valid IP or CIDR", + "pattern": "^([0-9]{1,3}[.]{1}){3}[0-9]{1,3}$" + }, + { + "description": "must be a valid IP or CIDR", + "pattern": "^([0-9]{1,3}[.]{1}){3}[0-9]{1,3}/[0-9]{1,3}$" + } + ], + "type": "string" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "maxItems": 16, + "type": "array" + }, + "strip_path": { + "default": true, + "type": "boolean" + }, + "tags": { + "items": { + "maxLength": 128, + "minLength": 1, + "pattern": "^(?:[0-9a-zA-Z.\\-_~:]+(?: *[0-9a-zA-Z.\\-_~:])*)?$", + "type": "string" + }, + "maxItems": 8, + "type": "array", + "uniqueItems": true + }, + "updated_at": { + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "id", + "protocols" + ], + "type": "object" +} diff --git a/kong/testdata/serviceJSONSchema.json b/kong/testdata/serviceJSONSchema.json new file mode 100644 index 000000000..02efeb47c --- /dev/null +++ b/kong/testdata/serviceJSONSchema.json @@ -0,0 +1,302 @@ +{ + "additionalProperties": false, + "allOf": [ + { + "description": "client_certificate can be set only when protocol is `https`", + "if": { + "required": [ + "client_certificate" + ] + }, + "then": { + "properties": { + "protocol": { + "const": "https" + } + }, + "required": [ + "protocol" + ] + }, + "title": "client_certificate_rule" + }, + { + "description": "tls_verify can be set only when protocol is `https`", + "if": { + "properties": { + "tls_verify": { + "const": true + } + }, + "required": [ + "tls_verify" + ] + }, + "then": { + "properties": { + "protocol": { + "const": "https" + } + }, + "required": [ + "protocol" + ] + }, + "title": "tls_verify_rule" + }, + { + "description": "tls_verify_depth can be set only when protocol is `https`", + "if": { + "required": [ + "tls_verify_depth" + ] + }, + "then": { + "properties": { + "protocol": { + "const": "https" + } + }, + "required": [ + "protocol" + ] + } + }, + { + "description": "ca_certificates can be set only when protocol is `https`", + "if": { + "required": [ + "ca_certificates" + ] + }, + "then": { + "properties": { + "protocol": { + "const": "https" + } + }, + "required": [ + "protocol" + ] + } + }, + { + "description": "path can be set only when protocol is 'http' or 'https'", + "if": { + "properties": { + "protocol": { + "oneOf": [ + { + "const": "grpc" + }, + { + "const": "grpcs" + }, + { + "const": "tcp" + }, + { + "const": "tls" + }, + { + "const": "udp" + } + ] + } + }, + "required": [ + "protocol" + ] + }, + "then": { + "properties": { + "path": { + "not": {} + } + } + } + }, + { + "description": "url should not be set", + "not": { + "required": [ + "url" + ] + } + }, + { + "description": "'ws' and 'wss' protocols are Kong Enterprise-only features. Please upgrade to Kong Enterprise to use this feature.", + "not": { + "properties": { + "protocol": { + "oneOf": [ + { + "const": "ws", + "type": "string" + }, + { + "const": "wss", + "type": "string" + } + ] + } + }, + "required": [ + "protocol" + ] + }, + "title": "ws_protocols_rule" + } + ], + "properties": { + "ca_certificates": { + "items": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + }, + "type": "array" + }, + "client_certificate": { + "additionalProperties": false, + "description": "foreign", + "properties": { + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "connect_timeout": { + "default": 60000, + "maximum": 2147483646, + "minimum": 1, + "type": "integer" + }, + "created_at": { + "minimum": 1, + "type": "integer" + }, + "enabled": { + "default": true, + "type": "boolean" + }, + "host": { + "description": "must be a valid hostname", + "maxLength": 256, + "pattern": "^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$", + "type": "string" + }, + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[0-9a-zA-Z.\\-_~]*$", + "type": "string" + }, + "path": { + "allOf": [ + { + "description": "must begin with `/`", + "pattern": "^/.*" + }, + { + "description": "length must not exceed 1024", + "maxLength": 1024 + }, + { + "not": { + "description": "must not contain `//`", + "pattern": "//" + } + } + ], + "type": "string" + }, + "port": { + "default": 80, + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "protocol": { + "default": "http", + "enum": [ + "http", + "https", + "grpc", + "grpcs", + "tcp", + "udp", + "tls", + "tls_passthrough", + "ws", + "wss" + ], + "type": "string" + }, + "read_timeout": { + "default": 60000, + "maximum": 2147483646, + "minimum": 1, + "type": "integer" + }, + "retries": { + "default": 5, + "maximum": 32767, + "minimum": 1, + "type": "integer" + }, + "tags": { + "items": { + "maxLength": 128, + "minLength": 1, + "pattern": "^(?:[0-9a-zA-Z.\\-_~:]+(?: *[0-9a-zA-Z.\\-_~:])*)?$", + "type": "string" + }, + "maxItems": 8, + "type": "array", + "uniqueItems": true + }, + "tls_verify": { + "type": "boolean" + }, + "tls_verify_depth": { + "maximum": 64, + "minimum": 0, + "type": "integer" + }, + "updated_at": { + "minimum": 1, + "type": "integer" + }, + "url": { + "type": "string" + }, + "write_timeout": { + "default": 60000, + "maximum": 2147483646, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "id", + "protocol", + "host", + "port", + "connect_timeout", + "read_timeout", + "write_timeout" + ], + "type": "object" +} diff --git a/kong/testdata/targetJSONSchema.json b/kong/testdata/targetJSONSchema.json new file mode 100644 index 000000000..1e3cd54e6 --- /dev/null +++ b/kong/testdata/targetJSONSchema.json @@ -0,0 +1,61 @@ +{ + "additionalProperties": false, + "properties": { + "created_at": { + "minimum": 1, + "type": "integer" + }, + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + }, + "tags": { + "items": { + "maxLength": 128, + "minLength": 1, + "pattern": "^(?:[0-9a-zA-Z.\\-_~:]+(?: *[0-9a-zA-Z.\\-_~:])*)?$", + "type": "string" + }, + "maxItems": 8, + "type": "array", + "uniqueItems": true + }, + "target": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "updated_at": { + "minimum": 1, + "type": "integer" + }, + "upstream": { + "additionalProperties": false, + "description": "foreign", + "properties": { + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "weight": { + "default": 100, + "maximum": 65535, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "id", + "target", + "upstream" + ], + "type": "object" +} diff --git a/kong/testdata/upstreamJSONSchema.json b/kong/testdata/upstreamJSONSchema.json new file mode 100644 index 000000000..0419c754d --- /dev/null +++ b/kong/testdata/upstreamJSONSchema.json @@ -0,0 +1,948 @@ +{ + "additionalProperties": false, + "allOf": [ + { + "description": "when 'hash_on' is set to 'header','hash_on_header' must be set", + "if": { + "properties": { + "hash_on": { + "const": "header" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "required": [ + "hash_on_header" + ] + } + }, + { + "description": "when 'hash_fallback' is set to 'header','hash_fallback_header' must be set", + "if": { + "properties": { + "hash_fallback": { + "const": "header" + } + }, + "required": [ + "hash_fallback" + ] + }, + "then": { + "required": [ + "hash_fallback_header" + ] + } + }, + { + "description": "when 'hash_on' is set to 'cookie', 'hash_on_cookie' must be set", + "if": { + "properties": { + "hash_on": { + "const": "cookie" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "required": [ + "hash_on_cookie" + ] + } + }, + { + "description": "when 'hash_fallback' is set to 'cookie', 'hash_on_cookie' must be set", + "if": { + "properties": { + "hash_fallback": { + "const": "cookie" + } + }, + "required": [ + "hash_fallback" + ] + }, + "then": { + "required": [ + "hash_on_cookie" + ] + } + }, + { + "description": "when 'hash_on' is set to 'none', 'hash_fallback' must be set to 'none'", + "if": { + "properties": { + "hash_on": { + "const": "none" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "properties": { + "hash_fallback": { + "const": "none" + } + }, + "required": [ + "hash_fallback" + ] + } + }, + { + "description": "when 'hash_on' is set to 'cookie', 'hash_fallback' must be set to 'none'", + "if": { + "properties": { + "hash_on": { + "const": "cookie" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "properties": { + "hash_fallback": { + "const": "none" + } + }, + "required": [ + "hash_fallback" + ] + } + }, + { + "description": "when 'hash_on' is set to 'consumer', 'hash_fallback' must be set to one of 'none', 'ip', 'header', 'cookie', 'path', 'query_arg', 'uri_capture'", + "if": { + "properties": { + "hash_on": { + "const": "consumer" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "properties": { + "hash_fallback": { + "anyOf": [ + { + "const": "none", + "type": "string" + }, + { + "const": "ip", + "type": "string" + }, + { + "const": "header", + "type": "string" + }, + { + "const": "cookie", + "type": "string" + }, + { + "const": "path", + "type": "string" + }, + { + "const": "query_arg", + "type": "string" + }, + { + "const": "uri_capture", + "type": "string" + } + ] + } + }, + "required": [ + "hash_fallback" + ] + } + }, + { + "description": "when 'hash_on' is set to 'ip', 'hash_fallback' must be set to one of 'none', 'consumer', 'header', 'cookie', 'path', 'query_arg', 'uri_capture'", + "if": { + "properties": { + "hash_on": { + "const": "ip" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "properties": { + "hash_fallback": { + "anyOf": [ + { + "const": "none", + "type": "string" + }, + { + "const": "consumer", + "type": "string" + }, + { + "const": "header", + "type": "string" + }, + { + "const": "cookie", + "type": "string" + }, + { + "const": "path", + "type": "string" + }, + { + "const": "query_arg", + "type": "string" + }, + { + "const": "uri_capture", + "type": "string" + } + ] + } + }, + "required": [ + "hash_fallback" + ] + } + }, + { + "description": "when 'hash_on' is set to 'path', 'hash_fallback' must be set to one of 'none', 'consumer', 'ip', 'header', 'cookie', 'query_arg', 'uri_capture'", + "if": { + "properties": { + "hash_on": { + "const": "path" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "properties": { + "hash_fallback": { + "anyOf": [ + { + "const": "none", + "type": "string" + }, + { + "const": "consumer", + "type": "string" + }, + { + "const": "header", + "type": "string" + }, + { + "const": "cookie", + "type": "string" + }, + { + "const": "ip", + "type": "string" + }, + { + "const": "query_arg", + "type": "string" + }, + { + "const": "uri_capture", + "type": "string" + } + ] + } + }, + "required": [ + "hash_fallback" + ] + } + }, + { + "description": "when 'hash_on' is set to 'query_arg', 'hash_on_query_arg' must be set", + "if": { + "properties": { + "hash_on": { + "const": "query_arg" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "required": [ + "hash_on_query_arg" + ] + } + }, + { + "description": "when 'hash_fallback' is set to 'query_arg', 'hash_fallback_query_arg' must be set", + "if": { + "properties": { + "hash_fallback": { + "const": "query_arg" + } + }, + "required": [ + "hash_fallback" + ] + }, + "then": { + "required": [ + "hash_fallback_query_arg" + ] + } + }, + { + "description": "when 'hash_on' is set to 'uri_capture', 'hash_on_uri_capture' must be set", + "if": { + "properties": { + "hash_on": { + "const": "uri_capture" + } + }, + "required": [ + "hash_on" + ] + }, + "then": { + "required": [ + "hash_on_uri_capture" + ] + } + }, + { + "description": "when 'hash_fallback' is set to 'uri_capture', 'hash_fallback_uri_capture' must be set", + "if": { + "properties": { + "hash_fallback": { + "const": "uri_capture" + } + }, + "required": [ + "hash_fallback" + ] + }, + "then": { + "required": [ + "hash_fallback_uri_capture" + ] + } + } + ], + "properties": { + "algorithm": { + "default": "round-robin", + "enum": [ + "round-robin", + "consistent-hashing", + "least-connections" + ], + "type": "string" + }, + "client_certificate": { + "additionalProperties": false, + "description": "foreign", + "properties": { + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "created_at": { + "minimum": 1, + "type": "integer" + }, + "hash_fallback": { + "default": "none", + "enum": [ + "none", + "consumer", + "ip", + "header", + "cookie", + "path", + "query_arg", + "uri_capture" + ], + "type": "string" + }, + "hash_fallback_header": { + "pattern": "^[A-Za-z0-9!#$%&'*+-.^_|~]{1,64}$", + "type": "string" + }, + "hash_fallback_query_arg": { + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_]+$", + "type": "string" + }, + "hash_fallback_uri_capture": { + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_]+$", + "type": "string" + }, + "hash_on": { + "default": "none", + "enum": [ + "none", + "consumer", + "ip", + "header", + "cookie", + "path", + "query_arg", + "uri_capture" + ], + "type": "string" + }, + "hash_on_cookie": { + "pattern": "^[a-zA-Z0-9-_]+$", + "type": "string" + }, + "hash_on_cookie_path": { + "allOf": [ + { + "description": "must begin with `/`", + "pattern": "^/.*" + }, + { + "description": "length must not exceed 1024", + "maxLength": 1024 + }, + { + "not": { + "description": "must not contain `//`", + "pattern": "//" + } + } + ], + "default": "/", + "type": "string" + }, + "hash_on_header": { + "pattern": "^[A-Za-z0-9!#$%&'*+-.^_|~]{1,64}$", + "type": "string" + }, + "hash_on_query_arg": { + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_]+$", + "type": "string" + }, + "hash_on_uri_capture": { + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_]+$", + "type": "string" + }, + "healthchecks": { + "default": { + "active": { + "concurrency": 10, + "healthy": { + "http_statuses": [ + 200, + 302 + ], + "interval": 0, + "successes": 0 + }, + "http_path": "/", + "https_verify_certificate": true, + "timeout": 1, + "type": "http", + "unhealthy": { + "http_failures": 0, + "http_statuses": [ + 429, + 404, + 500, + 501, + 502, + 503, + 504, + 505 + ], + "interval": 0, + "tcp_failures": 0, + "timeouts": 0 + } + }, + "passive": { + "healthy": { + "http_statuses": [ + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308 + ], + "successes": 0 + }, + "type": "http", + "unhealthy": { + "http_failures": 0, + "http_statuses": [ + 429, + 500, + 503 + ], + "tcp_failures": 0, + "timeouts": 0 + } + }, + "threshold": 0 + }, + "properties": { + "active": { + "default": { + "concurrency": 10, + "healthy": { + "http_statuses": [ + 200, + 302 + ], + "interval": 0, + "successes": 0 + }, + "http_path": "/", + "https_verify_certificate": true, + "timeout": 1, + "type": "http", + "unhealthy": { + "http_failures": 0, + "http_statuses": [ + 429, + 404, + 500, + 501, + 502, + 503, + 504, + 505 + ], + "interval": 0, + "tcp_failures": 0, + "timeouts": 0 + } + }, + "properties": { + "concurrency": { + "default": 10, + "maximum": 2147483648, + "minimum": 1, + "type": "integer" + }, + "healthy": { + "default": { + "http_statuses": [ + 200, + 302 + ], + "interval": 0, + "successes": 0 + }, + "properties": { + "http_statuses": { + "default": [ + 200, + 302 + ], + "items": { + "maximum": 999, + "minimum": 100, + "type": "integer" + }, + "maxItems": 32, + "type": "array" + }, + "interval": { + "default": 0, + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "successes": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + }, + "http_path": { + "allOf": [ + { + "description": "must begin with `/`", + "pattern": "^/.*" + }, + { + "description": "length must not exceed 1024", + "maxLength": 1024 + }, + { + "not": { + "description": "must not contain `//`", + "pattern": "//" + } + } + ], + "default": "/", + "type": "string" + }, + "https_sni": { + "description": "must be a valid hostname", + "maxLength": 256, + "pattern": "^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$", + "type": "string" + }, + "https_verify_certificate": { + "default": true, + "type": "boolean" + }, + "timeout": { + "default": 1, + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": { + "default": "http", + "enum": [ + "tcp", + "http", + "https", + "grpc", + "grpcs" + ], + "type": "string" + }, + "unhealthy": { + "default": { + "http_failures": 0, + "http_statuses": [ + 429, + 404, + 500, + 501, + 502, + 503, + 504, + 505 + ], + "interval": 0, + "tcp_failures": 0, + "timeouts": 0 + }, + "properties": { + "http_failures": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "http_statuses": { + "default": [ + 429, + 404, + 500, + 501, + 502, + 503, + 504, + 505 + ], + "items": { + "maximum": 999, + "minimum": 100, + "type": "integer" + }, + "maxItems": 32, + "type": "array" + }, + "interval": { + "default": 0, + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "tcp_failures": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "timeouts": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "passive": { + "default": { + "healthy": { + "http_statuses": [ + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308 + ], + "successes": 0 + }, + "type": "http", + "unhealthy": { + "http_failures": 0, + "http_statuses": [ + 429, + 500, + 503 + ], + "tcp_failures": 0, + "timeouts": 0 + } + }, + "properties": { + "healthy": { + "default": { + "http_statuses": [ + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308 + ], + "successes": 0 + }, + "properties": { + "http_statuses": { + "default": [ + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308 + ], + "items": { + "maximum": 999, + "minimum": 100, + "type": "integer" + }, + "maxItems": 32, + "type": "array" + }, + "successes": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + }, + "timeout": { + "default": 1, + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": { + "default": "http", + "enum": [ + "tcp", + "http", + "https", + "grpc", + "grpcs" + ], + "type": "string" + }, + "unhealthy": { + "default": { + "http_failures": 0, + "http_statuses": [ + 429, + 500, + 503 + ], + "tcp_failures": 0, + "timeouts": 0 + }, + "properties": { + "http_failures": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "http_statuses": { + "default": [ + 429, + 500, + 503 + ], + "items": { + "maximum": 999, + "minimum": 100, + "type": "integer" + }, + "maxItems": 32, + "type": "array" + }, + "tcp_failures": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "timeouts": { + "default": 0, + "maximum": 255, + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "threshold": { + "default": 0, + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "host_header": { + "description": "must be a valid hostname", + "maxLength": 256, + "pattern": "^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$", + "type": "string" + }, + "id": { + "description": "must be a valid UUID", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[0-9a-zA-Z.\\-_~]*$", + "type": "string" + }, + "slots": { + "default": 10000, + "maximum": 65536, + "minimum": 10, + "type": "integer" + }, + "tags": { + "items": { + "maxLength": 128, + "minLength": 1, + "pattern": "^(?:[0-9a-zA-Z.\\-_~:]+(?: *[0-9a-zA-Z.\\-_~:])*)?$", + "type": "string" + }, + "maxItems": 8, + "type": "array", + "uniqueItems": true + }, + "updated_at": { + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" +} diff --git a/kong/utils.go b/kong/utils.go index f649421c0..378c596da 100644 --- a/kong/utils.go +++ b/kong/utils.go @@ -248,7 +248,9 @@ func fillConfigRecord(schema gjson.Result, config Configuration) Configuration { // and flattens it, turning it into a map that can be more easily unmarshalled // into proper entity objects. // -// Sample input: +// This supports both Lua and JSON schemas. +// +// Sample Lua schema input: // // { // "fields": [ @@ -274,6 +276,32 @@ func fillConfigRecord(schema gjson.Result, config Configuration) Configuration { // ... // } // +// Sample JSON schema input: +// +// { +// "properties": [ +// { +// "algorithm": { +// "default": "round-robin", +// "enum": ["consistent-hashing", "least-connections", "round-robin"], +// "type": "string" +// } +// }, { +// "hash_on": { +// "default": "none", +// "enum": ["none", "consumer", "ip", "header", "cookie"], +// "type": "string" +// } +// }, { +// "hash_fallback": { +// "default": "none", +// "enum": ["none", "consumer", "ip", "header", "cookie"], +// "type": "string" +// } +// }, +// ... +// } +// // Sample output: // // { @@ -283,7 +311,49 @@ func fillConfigRecord(schema gjson.Result, config Configuration) Configuration { // ... // } func flattenDefaultsSchema(schema gjson.Result) Schema { - value := schema.Get("fields") + fields := schema.Get("fields") + if fields.Exists() { + return flattenLuaSchema(fields) + } + properties := schema.Get("properties") + if properties.Exists() { + return flattenJSONSchema(properties) + } + return Schema{} +} + +func flattenJSONSchema(value gjson.Result) Schema { + results := Schema{} + + value.ForEach(func(key, value gjson.Result) bool { + name := key.String() + + ftype := value.Get("type") + // when type==object and additionalProperties==false, the object + // represents either a foreign relationship or a map entry. + // In both cases, defaults don't need to be injected. + additionalProperties := value.Get("additionalProperties") + if ftype.String() == "object" && + (!additionalProperties.Exists() || + (additionalProperties.Exists() && + additionalProperties.Bool())) { + newSubConfig := flattenDefaultsSchema(value) + results[name] = newSubConfig + return true + } + value = value.Get("default") + if value.Exists() { + results[name] = value.Value() + } else { + results[name] = nil + } + return true + }) + + return results +} + +func flattenLuaSchema(value gjson.Result) Schema { results := Schema{} value.ForEach(func(key, value gjson.Result) bool { diff --git a/kong/utils_test.go b/kong/utils_test.go index e63a04331..23d3f6ed2 100644 --- a/kong/utils_test.go +++ b/kong/utils_test.go @@ -1,8 +1,11 @@ package kong import ( + "encoding/json" + "io" "net/http" "net/http/httptest" + "os" "reflect" "testing" @@ -567,6 +570,363 @@ func TestFillUpstreamsDefaults(T *testing.T) { } } +func getJSONSchemaFromFile(filename string) Schema { + jsonFile, err := os.Open(filename) + if err != nil { + panic(err) + } + defer jsonFile.Close() + + var schema Schema + byteValue, err := io.ReadAll(jsonFile) + if err != nil { + panic(err) + } + if err := json.Unmarshal(byteValue, &schema); err != nil { + panic(err) + } + return schema +} + +func TestFillUpstreamsDefaultsFromJSONSchema(t *testing.T) { + // load upstream JSON schema from local file. + schema := getJSONSchemaFromFile("testdata/upstreamJSONSchema.json") + + tests := []struct { + name string + upstream *Upstream + expected *Upstream + }{ + { + name: "fills defaults for all fields, leaves name unchanged", + upstream: &Upstream{ + Name: String("upstream1"), + }, + expected: &Upstream{ + Name: String("upstream1"), + Algorithm: String("round-robin"), + Slots: Int(10000), + Healthchecks: &Healthcheck{ + Active: &ActiveHealthcheck{ + Concurrency: Int(10), + Healthy: &Healthy{ + HTTPStatuses: []int{200, 302}, + Interval: Int(0), + Successes: Int(0), + }, + HTTPPath: String("/"), + HTTPSVerifyCertificate: Bool(true), + Type: String("http"), + Timeout: Int(1), + Unhealthy: &Unhealthy{ + HTTPFailures: Int(0), + HTTPStatuses: []int{ + 429, 404, + 500, 501, 502, 503, 504, 505, + }, + TCPFailures: Int(0), + Timeouts: Int(0), + Interval: Int(0), + }, + }, + Passive: &PassiveHealthcheck{ + Healthy: &Healthy{ + HTTPStatuses: []int{ + 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, + 300, 301, 302, 303, 304, 305, 306, 307, 308, + }, + Successes: Int(0), + }, + Type: String("http"), + Unhealthy: &Unhealthy{ + HTTPFailures: Int(0), + HTTPStatuses: []int{429, 500, 503}, + TCPFailures: Int(0), + Timeouts: Int(0), + }, + }, + }, + HashOn: String("none"), + HashFallback: String("none"), + HashOnCookiePath: String("/"), + }, + }, + { + name: "fills defaults for all fields except algorithm and hash_on, leaves name unchanged", + upstream: &Upstream{ + Name: String("upstream1"), + Algorithm: String("consistent-hashing"), + HashOn: String("ip"), + }, + expected: &Upstream{ + Name: String("upstream1"), + Algorithm: String("consistent-hashing"), + Slots: Int(10000), + Healthchecks: &Healthcheck{ + Active: &ActiveHealthcheck{ + Concurrency: Int(10), + Healthy: &Healthy{ + HTTPStatuses: []int{200, 302}, + Interval: Int(0), + Successes: Int(0), + }, + HTTPPath: String("/"), + HTTPSVerifyCertificate: Bool(true), + Type: String("http"), + Timeout: Int(1), + Unhealthy: &Unhealthy{ + HTTPFailures: Int(0), + HTTPStatuses: []int{ + 429, 404, + 500, 501, 502, 503, 504, 505, + }, + TCPFailures: Int(0), + Timeouts: Int(0), + Interval: Int(0), + }, + }, + Passive: &PassiveHealthcheck{ + Healthy: &Healthy{ + HTTPStatuses: []int{ + 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, + 300, 301, 302, 303, 304, 305, 306, 307, 308, + }, + Successes: Int(0), + }, + Type: String("http"), + Unhealthy: &Unhealthy{ + HTTPFailures: Int(0), + HTTPStatuses: []int{429, 500, 503}, + TCPFailures: Int(0), + Timeouts: Int(0), + }, + }, + }, + HashOn: String("ip"), + HashFallback: String("none"), + HashOnCookiePath: String("/"), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + u := tc.upstream + if err := FillEntityDefaults(u, schema); err != nil { + t.Errorf(err.Error()) + } + // Ignore fields to make tests pass despite small differences across releases. + opts := cmpopts.IgnoreFields(Healthcheck{}, "Threshold") + if diff := cmp.Diff(u, tc.expected, opts); diff != "" { + t.Errorf(diff) + } + }) + } +} + +func TestFillServicesDefaultsFromJSONSchema(t *testing.T) { + // load service JSON schema from local file. + schema := getJSONSchemaFromFile("testdata/serviceJSONSchema.json") + + tests := []struct { + name string + service *Service + expected *Service + }{ + { + name: "fills defaults for all fields, leaves name and host unchanged", + service: &Service{ + Name: String("svc1"), + Host: String("mockbin.org"), + }, + expected: &Service{ + Name: String("svc1"), + Host: String("mockbin.org"), + Port: Int(80), + Protocol: String("http"), + ConnectTimeout: Int(60000), + ReadTimeout: Int(60000), + Retries: Int(5), + WriteTimeout: Int(60000), + }, + }, + { + name: "fills defaults for all fields except port, leaves name and host unchanged", + service: &Service{ + Name: String("svc1"), + Host: String("mockbin.org"), + Port: Int(8080), + }, + expected: &Service{ + Name: String("svc1"), + Host: String("mockbin.org"), + Port: Int(8080), + Protocol: String("http"), + ConnectTimeout: Int(60000), + ReadTimeout: Int(60000), + Retries: Int(5), + WriteTimeout: Int(60000), + }, + }, + { + name: "fills defaults for all fields except port, leaves name, tags and host unchanged", + service: &Service{ + Name: String("svc1"), + Host: String("mockbin.org"), + Port: Int(8080), + Tags: []*string{String("tag1"), String("tag2")}, + }, + expected: &Service{ + Name: String("svc1"), + Host: String("mockbin.org"), + Port: Int(8080), + Protocol: String("http"), + ConnectTimeout: Int(60000), + ReadTimeout: Int(60000), + Retries: Int(5), + WriteTimeout: Int(60000), + Tags: []*string{String("tag1"), String("tag2")}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + s := tc.service + if err := FillEntityDefaults(s, schema); err != nil { + t.Errorf(err.Error()) + } + opt := []cmp.Option{ + cmpopts.IgnoreFields(Service{}, "Enabled"), + } + if diff := cmp.Diff(s, tc.expected, opt...); diff != "" { + t.Errorf(diff) + } + }) + } +} + +func TestFillRoutesDefaultsFromJSONSchema(t *testing.T) { + // load route JSON schema from local file. + schema := getJSONSchemaFromFile("testdata/routeJSONSchema.json") + + tests := []struct { + name string + route *Route + expected *Route + }{ + { + name: "fills defaults for all fields except paths, leaves name unchanged", + route: &Route{ + Name: String("r1"), + Paths: []*string{String("/r1")}, + }, + expected: &Route{ + Name: String("r1"), + Paths: []*string{String("/r1")}, + PreserveHost: Bool(false), + Protocols: []*string{String("http"), String("https")}, + RegexPriority: Int(0), + StripPath: Bool(true), + HTTPSRedirectStatusCode: Int(426), + }, + }, + { + name: "fills defaults for all fields except paths and protocols, leaves name unchanged", + route: &Route{ + Name: String("r1"), + Paths: []*string{String("/r1")}, + Protocols: []*string{String("grpc")}, + }, + expected: &Route{ + Name: String("r1"), + Paths: []*string{String("/r1")}, + PreserveHost: Bool(false), + Protocols: []*string{String("grpc")}, + RegexPriority: Int(0), + StripPath: Bool(true), + HTTPSRedirectStatusCode: Int(426), + }, + }, + { + name: "boolean default values don't overwrite existing fields if set", + route: &Route{ + Name: String("r1"), + Paths: []*string{String("/r1")}, + Protocols: []*string{String("grpc")}, + StripPath: Bool(false), + PreserveHost: Bool(true), + }, + expected: &Route{ + Name: String("r1"), + Paths: []*string{String("/r1")}, + PreserveHost: Bool(true), + Protocols: []*string{String("grpc")}, + RegexPriority: Int(0), + StripPath: Bool(false), + HTTPSRedirectStatusCode: Int(426), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + r := tc.route + if err := FillEntityDefaults(r, schema); err != nil { + t.Errorf(err.Error()) + } + // Ignore fields to make tests pass despite small differences across releases. + opts := cmpopts.IgnoreFields( + Route{}, + "RequestBuffering", "ResponseBuffering", "PathHandling", + ) + if diff := cmp.Diff(r, tc.expected, opts); diff != "" { + t.Errorf(diff) + } + }) + } +} + +func TestFillTargetDefaultsFromJSONSchema(t *testing.T) { + // load route JSON schema from local file. + schema := getJSONSchemaFromFile("testdata/targetJSONSchema.json") + + tests := []struct { + name string + target *Target + expected *Target + }{ + { + name: "fills default for weight", + target: &Target{}, + expected: &Target{ + Weight: Int(100), + }, + }, + { + name: "set zero-value", + target: &Target{ + Weight: Int(0), + }, + expected: &Target{ + Weight: Int(0), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + target := tc.target + if err := FillEntityDefaults(target, schema); err != nil { + t.Errorf(err.Error()) + } + if diff := cmp.Diff(target, tc.expected); diff != "" { + t.Errorf(diff) + } + }) + } +} + func TestHTTPClientWithHeaders(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200)