Skip to content

Commit 36182d0

Browse files
fix(dbless): nest duplicate entity primary key errors for children (#8891)
This makes a change to the shape/structure of dbless errors. Before this change, an upstream with duplicate targets would yield a response like this: ``` { "code": 14, "fields": { "targets": [ null, "uniqueness violation: 'targets' entity with primary key set to '48322e4a-b3b0-591b-8ed6-fd95a6d75019' already declared" ] }, "message": "declarative config is invalid: {targets={[2]=\"uniqueness violation: 'targets' entity with primary key set to '48322e4a-b3b0-591b-8ed6-fd95a6d75019' already declared\"}}", "name": "invalid declarative configuration" } ``` After this change, the errors are nested under the parent upstream: ``` { "code": 14, "fields": { "upstreams": [ null, { "targets": [ null, "uniqueness violation: 'targets' entity with primary key set to '48322e4a-b3b0-591b-8ed6-fd95a6d75019' already declared" ] } ] }, "message": "declarative config is invalid: {upstreams={[2]={targets={[2]=\"uniqueness violation: 'targets' entity with primary key set to '48322e4a-b3b0-591b-8ed6-fd95a6d75019' already declared\"}}}}", "name": "invalid declarative configuration" } ``` As a result, the error can now be properly mapped to the input target, so `POST /config?flatten_errors=1` returns a helpful result: ``` { "code": 14, "fields": {}, "flattened_errors": [ { "entity": { "id": "48322e4a-b3b0-591b-8ed6-fd95a6d75019", "tags": [ "target-2" ], "target": "10.244.0.12:80", "upstream": { "id": "f9792964-6797-482c-bfdf-08220a4f6839" }, "weight": 1 }, "entity_id": "48322e4a-b3b0-591b-8ed6-fd95a6d75019", "entity_tags": [ "target-2" ], "entity_type": "target", "errors": [ { "message": "uniqueness violation: 'targets' entity with primary key set to '48322e4a-b3b0-591b-8ed6-fd95a6d75019' already declared", "type": "entity" } ] } ], "message": "declarative config is invalid: {}", "name": "invalid declarative configuration" } ``` (cherry picked from commit b8891eb) Co-authored-by: Michael Martin <[email protected]>
1 parent a44bcdb commit 36182d0

File tree

4 files changed

+141
-13
lines changed

4 files changed

+141
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
message: "Fixed an issue wherein `POST /config?flatten_errors=1` could not return a proper response if the input included duplicate upstream targets"
2+
type: bugfix
3+
scope: Core

kong/db/errors.lua

+10-9
Original file line numberDiff line numberDiff line change
@@ -835,12 +835,19 @@ do
835835
---@param err_t table
836836
---@param flattened table
837837
local function add_entity_errors(entity_type, entity, err_t, flattened)
838-
if type(err_t) ~= "table" or nkeys(err_t) == 0 then
838+
local err_type = type(err_t)
839+
840+
-- promote error strings to `@entity` type errors
841+
if err_type == "string" then
842+
err_t = { ["@entity"] = err_t }
843+
844+
elseif err_type ~= "table" or nkeys(err_t) == 0 then
839845
return
846+
end
840847

841848
-- this *should* be unreachable, but it's relatively cheap to guard against
842849
-- compared to everything else we're doing in this code path
843-
elseif type(entity) ~= "table" then
850+
if type(entity) ~= "table" then
844851
log(WARN, "could not parse ", entity_type, " errors for non-table ",
845852
"input: '", tostring(entity), "'")
846853
return
@@ -1073,13 +1080,7 @@ do
10731080
for i, err_t_i in drain(section_errors) do
10741081
local entity = entities[i]
10751082

1076-
1077-
-- promote error strings to `@entity` type errors
1078-
if type(err_t_i) == "string" then
1079-
err_t_i = { ["@entity"] = err_t_i }
1080-
end
1081-
1082-
if type(entity) == "table" and type(err_t_i) == "table" then
1083+
if type(entity) == "table" then
10831084
add_entity_errors(entity_type, entity, err_t_i, flattened)
10841085

10851086
else

kong/db/schema/others/declarative_config.lua

+19-4
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ local function uniqueness_error_msg(entity, key, value)
352352
end
353353

354354

355-
local function populate_references(input, known_entities, by_id, by_key, expected, errs, parent_entity)
355+
local function populate_references(input, known_entities, by_id, by_key, expected, errs, parent_entity, parent_idx)
356356
for _, entity in ipairs(known_entities) do
357357
yield(true)
358358

@@ -380,7 +380,7 @@ local function populate_references(input, known_entities, by_id, by_key, expecte
380380
for i, item in ipairs(input[entity]) do
381381
yield(true)
382382

383-
populate_references(item, known_entities, by_id, by_key, expected, errs, entity)
383+
populate_references(item, known_entities, by_id, by_key, expected, errs, entity, i)
384384

385385
local item_id = DeclarativeConfig.pk_string(entity_schema, item)
386386
local key = use_key and item[endpoint_key]
@@ -398,8 +398,23 @@ local function populate_references(input, known_entities, by_id, by_key, expecte
398398
if item_id then
399399
by_id[entity] = by_id[entity] or {}
400400
if (not failed) and by_id[entity][item_id] then
401-
errs[entity] = errs[entity] or {}
402-
errs[entity][i] = uniqueness_error_msg(entity, "primary key", item_id)
401+
local err_t
402+
403+
if parent_entity and parent_idx then
404+
errs[parent_entity] = errs[parent_entity] or {}
405+
errs[parent_entity][parent_idx] = errs[parent_entity][parent_idx] or {}
406+
errs[parent_entity][parent_idx][entity] = errs[parent_entity][parent_idx][entity] or {}
407+
408+
-- e.g. errs["upstreams"][5]["targets"]
409+
err_t = errs[parent_entity][parent_idx][entity]
410+
411+
else
412+
errs[entity] = errs[entity] or {}
413+
err_t = errs[entity]
414+
end
415+
416+
err_t[i] = uniqueness_error_msg(entity, "primary key", item_id)
417+
403418
else
404419
by_id[entity][item_id] = item
405420
table.insert(by_id[entity], item_id)

spec/02-integration/04-admin_api/15-off_spec.lua

+109
Original file line numberDiff line numberDiff line change
@@ -2773,6 +2773,115 @@ R6InCcH2Wh8wSeY5AuDXvu2tv9g/PW9wIJmPuKSHMA==
27732773
},
27742774
}, flattened)
27752775
end)
2776+
2777+
it("correctly handles duplicate upstream target errors", function()
2778+
local target = {
2779+
target = "10.244.0.12:80",
2780+
weight = 1,
2781+
tags = { "target-1" },
2782+
}
2783+
-- this has the same <addr>:<port> tuple as the first target, so it will
2784+
-- be assigned the same id
2785+
local dupe_target = utils.deep_copy(target)
2786+
dupe_target.tags = { "target-2" }
2787+
2788+
local input = {
2789+
_format_version = "3.0",
2790+
services = {
2791+
{
2792+
connect_timeout = 60000,
2793+
host = "httproute.default.httproute-testing.0",
2794+
id = "4e3cb785-a8d0-5866-aa05-117f7c64f24d",
2795+
name = "httproute.default.httproute-testing.0",
2796+
port = 8080,
2797+
protocol = "http",
2798+
read_timeout = 60000,
2799+
retries = 5,
2800+
routes = {
2801+
{
2802+
https_redirect_status_code = 426,
2803+
id = "073fc413-1c03-50b4-8f44-43367c13daba",
2804+
name = "httproute.default.httproute-testing.0.0",
2805+
path_handling = "v0",
2806+
paths = {
2807+
"~/httproute-testing$",
2808+
"/httproute-testing/",
2809+
},
2810+
preserve_host = true,
2811+
protocols = {
2812+
"http",
2813+
"https",
2814+
},
2815+
strip_path = true,
2816+
tags = {},
2817+
},
2818+
},
2819+
tags = {},
2820+
write_timeout = 60000,
2821+
},
2822+
},
2823+
upstreams = {
2824+
{
2825+
algorithm = "round-robin",
2826+
name = "httproute.default.httproute-testing.0",
2827+
id = "e9792964-6797-482c-bfdf-08220a4f6832",
2828+
tags = {
2829+
"k8s-name:httproute-testing",
2830+
"k8s-namespace:default",
2831+
"k8s-kind:HTTPRoute",
2832+
"k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839",
2833+
"k8s-group:gateway.networking.k8s.io",
2834+
"k8s-version:v1",
2835+
},
2836+
targets = {
2837+
{
2838+
target = "10.244.0.11:80",
2839+
weight = 1,
2840+
},
2841+
{
2842+
target = "10.244.0.12:80",
2843+
weight = 1,
2844+
},
2845+
},
2846+
},
2847+
{
2848+
algorithm = "round-robin",
2849+
name = "httproute.default.httproute-testing.1",
2850+
id = "f9792964-6797-482c-bfdf-08220a4f6839",
2851+
tags = {
2852+
"k8s-name:httproute-testing",
2853+
"k8s-namespace:default",
2854+
"k8s-kind:HTTPRoute",
2855+
"k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839",
2856+
"k8s-group:gateway.networking.k8s.io",
2857+
"k8s-version:v1",
2858+
},
2859+
targets = {
2860+
target,
2861+
dupe_target,
2862+
},
2863+
},
2864+
},
2865+
}
2866+
2867+
local flattened = post_config(input)
2868+
local entry = get_by_tag(dupe_target.tags[1], flattened)
2869+
assert.not_nil(entry, "no error for duplicate target in the response")
2870+
2871+
-- sanity
2872+
assert.same(dupe_target.tags, entry.entity_tags)
2873+
2874+
assert.is_table(entry.errors, "missing entity errors table")
2875+
assert.equals(1, #entry.errors, "expected 1 entity error")
2876+
assert.is_table(entry.errors[1], "entity error is not a table")
2877+
2878+
local e = entry.errors[1]
2879+
assert.equals("entity", e.type)
2880+
2881+
local exp = string.format("uniqueness violation: 'targets' entity with primary key set to '%s' already declared", entry.entity_id)
2882+
2883+
assert.equals(exp, e.message)
2884+
end)
27762885
end)
27772886

27782887

0 commit comments

Comments
 (0)