Skip to content

Commit b8891eb

Browse files
flrghjschmid1
authored andcommitted
fix(dbless): nest duplicate entity primary key errors for children
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" } ```
1 parent 0d81517 commit b8891eb

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
@@ -795,12 +795,19 @@ do
795795
---@param err_t table
796796
---@param flattened table
797797
local function add_entity_errors(entity_type, entity, err_t, flattened)
798-
if type(err_t) ~= "table" or nkeys(err_t) == 0 then
798+
local err_type = type(err_t)
799+
800+
-- promote error strings to `@entity` type errors
801+
if err_type == "string" then
802+
err_t = { ["@entity"] = err_t }
803+
804+
elseif err_type ~= "table" or nkeys(err_t) == 0 then
799805
return
806+
end
800807

801808
-- this *should* be unreachable, but it's relatively cheap to guard against
802809
-- compared to everything else we're doing in this code path
803-
elseif type(entity) ~= "table" then
810+
if type(entity) ~= "table" then
804811
log(WARN, "could not parse ", entity_type, " errors for non-table ",
805812
"input: '", tostring(entity), "'")
806813
return
@@ -1033,13 +1040,7 @@ do
10331040
for i, err_t_i in drain(section_errors) do
10341041
local entity = entities[i]
10351042

1036-
1037-
-- promote error strings to `@entity` type errors
1038-
if type(err_t_i) == "string" then
1039-
err_t_i = { ["@entity"] = err_t_i }
1040-
end
1041-
1042-
if type(entity) == "table" and type(err_t_i) == "table" then
1043+
if type(entity) == "table" then
10431044
add_entity_errors(entity_type, entity, err_t_i, flattened)
10441045

10451046
else

kong/db/schema/others/declarative_config.lua

+19-4
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ local function uniqueness_error_msg(entity, key, value)
335335
end
336336

337337

338-
local function populate_references(input, known_entities, by_id, by_key, expected, errs, parent_entity)
338+
local function populate_references(input, known_entities, by_id, by_key, expected, errs, parent_entity, parent_idx)
339339
for _, entity in ipairs(known_entities) do
340340
yield(true)
341341

@@ -363,7 +363,7 @@ local function populate_references(input, known_entities, by_id, by_key, expecte
363363
for i, item in ipairs(input[entity]) do
364364
yield(true)
365365

366-
populate_references(item, known_entities, by_id, by_key, expected, errs, entity)
366+
populate_references(item, known_entities, by_id, by_key, expected, errs, entity, i)
367367

368368
local item_id = DeclarativeConfig.pk_string(entity_schema, item)
369369
local key = use_key and item[endpoint_key]
@@ -381,8 +381,23 @@ local function populate_references(input, known_entities, by_id, by_key, expecte
381381
if item_id then
382382
by_id[entity] = by_id[entity] or {}
383383
if (not failed) and by_id[entity][item_id] then
384-
errs[entity] = errs[entity] or {}
385-
errs[entity][i] = uniqueness_error_msg(entity, "primary key", item_id)
384+
local err_t
385+
386+
if parent_entity and parent_idx then
387+
errs[parent_entity] = errs[parent_entity] or {}
388+
errs[parent_entity][parent_idx] = errs[parent_entity][parent_idx] or {}
389+
errs[parent_entity][parent_idx][entity] = errs[parent_entity][parent_idx][entity] or {}
390+
391+
-- e.g. errs["upstreams"][5]["targets"]
392+
err_t = errs[parent_entity][parent_idx][entity]
393+
394+
else
395+
errs[entity] = errs[entity] or {}
396+
err_t = errs[entity]
397+
end
398+
399+
err_t[i] = uniqueness_error_msg(entity, "primary key", item_id)
400+
386401
else
387402
by_id[entity][item_id] = item
388403
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
@@ -2724,6 +2724,115 @@ R6InCcH2Wh8wSeY5AuDXvu2tv9g/PW9wIJmPuKSHMA==
27242724
},
27252725
}, flattened)
27262726
end)
2727+
2728+
it("correctly handles duplicate upstream target errors", function()
2729+
local target = {
2730+
target = "10.244.0.12:80",
2731+
weight = 1,
2732+
tags = { "target-1" },
2733+
}
2734+
-- this has the same <addr>:<port> tuple as the first target, so it will
2735+
-- be assigned the same id
2736+
local dupe_target = utils.deep_copy(target)
2737+
dupe_target.tags = { "target-2" }
2738+
2739+
local input = {
2740+
_format_version = "3.0",
2741+
services = {
2742+
{
2743+
connect_timeout = 60000,
2744+
host = "httproute.default.httproute-testing.0",
2745+
id = "4e3cb785-a8d0-5866-aa05-117f7c64f24d",
2746+
name = "httproute.default.httproute-testing.0",
2747+
port = 8080,
2748+
protocol = "http",
2749+
read_timeout = 60000,
2750+
retries = 5,
2751+
routes = {
2752+
{
2753+
https_redirect_status_code = 426,
2754+
id = "073fc413-1c03-50b4-8f44-43367c13daba",
2755+
name = "httproute.default.httproute-testing.0.0",
2756+
path_handling = "v0",
2757+
paths = {
2758+
"~/httproute-testing$",
2759+
"/httproute-testing/",
2760+
},
2761+
preserve_host = true,
2762+
protocols = {
2763+
"http",
2764+
"https",
2765+
},
2766+
strip_path = true,
2767+
tags = {},
2768+
},
2769+
},
2770+
tags = {},
2771+
write_timeout = 60000,
2772+
},
2773+
},
2774+
upstreams = {
2775+
{
2776+
algorithm = "round-robin",
2777+
name = "httproute.default.httproute-testing.0",
2778+
id = "e9792964-6797-482c-bfdf-08220a4f6832",
2779+
tags = {
2780+
"k8s-name:httproute-testing",
2781+
"k8s-namespace:default",
2782+
"k8s-kind:HTTPRoute",
2783+
"k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839",
2784+
"k8s-group:gateway.networking.k8s.io",
2785+
"k8s-version:v1",
2786+
},
2787+
targets = {
2788+
{
2789+
target = "10.244.0.11:80",
2790+
weight = 1,
2791+
},
2792+
{
2793+
target = "10.244.0.12:80",
2794+
weight = 1,
2795+
},
2796+
},
2797+
},
2798+
{
2799+
algorithm = "round-robin",
2800+
name = "httproute.default.httproute-testing.1",
2801+
id = "f9792964-6797-482c-bfdf-08220a4f6839",
2802+
tags = {
2803+
"k8s-name:httproute-testing",
2804+
"k8s-namespace:default",
2805+
"k8s-kind:HTTPRoute",
2806+
"k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839",
2807+
"k8s-group:gateway.networking.k8s.io",
2808+
"k8s-version:v1",
2809+
},
2810+
targets = {
2811+
target,
2812+
dupe_target,
2813+
},
2814+
},
2815+
},
2816+
}
2817+
2818+
local flattened = post_config(input)
2819+
local entry = get_by_tag(dupe_target.tags[1], flattened)
2820+
assert.not_nil(entry, "no error for duplicate target in the response")
2821+
2822+
-- sanity
2823+
assert.same(dupe_target.tags, entry.entity_tags)
2824+
2825+
assert.is_table(entry.errors, "missing entity errors table")
2826+
assert.equals(1, #entry.errors, "expected 1 entity error")
2827+
assert.is_table(entry.errors[1], "entity error is not a table")
2828+
2829+
local e = entry.errors[1]
2830+
assert.equals("entity", e.type)
2831+
2832+
local exp = string.format("uniqueness violation: 'targets' entity with primary key set to '%s' already declared", entry.entity_id)
2833+
2834+
assert.equals(exp, e.message)
2835+
end)
27272836
end)
27282837

27292838

0 commit comments

Comments
 (0)