-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(core) implement entity tagging #4275
Conversation
kong/api/endpoints.lua
Outdated
options.tags_cond = 'or' | ||
options.tags = split(tags, '/') | ||
else | ||
options.tags = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, should the:
options.tags = tags
-- or
options.tags = args.tags
So that later on, when we validate, we have original faulty input there, which can make a better error message?
kong/db/dao/init.lua
Outdated
elseif type(options.tags) ~= "table" then | ||
errors.tags = "must be a table" | ||
elseif #options.tags > 5 then | ||
errors.tags = "cannot be query more than 5 tags" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cannot be query more than 5 tags
-> cannot query more than 5 tags
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fffonion This is an amazing piece of work! It does cover a lot:
- Core:
1.1. a pretty regular-lookingtags
attribute in core entity schemas (which triggers special behavior in the DAO when modified), which is currently injected by the magictags = true
1.2. a read-onlyTags
entity (which is only updated via side-effects on thetags
attribute of other entities), used to support thepage_by_tags
1.3. new method optionsopts.tags
andopts.tags_cond
which filter on core entity searches - API:
2.1. new queryargs?tags=foo,bar
and?tags=foo/bar
(I hope we don't need to support/
in tag names in the future!)
2.2. new endpoint/tags/:tags
I sprinkled comments all over, but it's mostly typos and missing tests (e.g. 2.2). We need to have more tests for the failing cases. And the Tags DAO object needs to be protected from destructive operations.
At this point I wanted to avoid "designy" questions as I think the design and implementation are pretty well rounded at this point. The only thing sticking out to me that I couldn't help but bring up is the magic tags = true
attribute, because at this point I think we don't really need it:
- If it was added to prevent
tags
from being incorrectly specified in schemas: the codebase also assumes a particular format forid
and that doesn't have special construction; the schema for Entities inkong.db.schema.entity
could double-check it if necessary. - If it was added to make it easy to test if a schema is tags-enabled:
schema.fields.tags
works just as well asschema.tags
.
Maybe this design stems from earlier when tags were more special and less of a regular field? They are currently a regular field in schemas, in DB tables, in the DAO object... we totally could do without special casing it in the schema level.
I think that wouldn't be a significant change at this point (it would be more removing code than adding), but I think it would be an improvement. I'm actually more concerned about the DAO protections and missing tests; I feel these would give us extra confidence for the merge.
kong/db/dao/init.lua
Outdated
elseif not match(concat(options.tags), "^[%w%.%-%_~]+$") then | ||
errors.tags = "must only contain alphanumeric and '., -, _, ~' characters" | ||
elseif #options.tags > 1 and options.tags_cond ~= "and" and options.tags_cond ~= "or" then | ||
errors.tags_cond = "must be a either 'and' or 'or' when more than one tags are specified" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
more than one tags are specified
->more than one tag is specified
kong/db/strategies/postgres/tags.lua
Outdated
|
||
local sql_templates = { | ||
page_first = [[ | ||
SELECT entity_id, entity_name, tag, ordinarity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo? ordinarity
end | ||
end) | ||
|
||
it("list all entities attached with tag", function() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
list all entities attached with tag
this doesn't list all entities attached with a given tag, but rather "list all entities that have tags", right? If so, the latter could be a clearer description for the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, have tags
will describe it clearer
@@ -145,6 +146,24 @@ local function validate_options_value(options, schema, context) | |||
errors.ttl = fmt("cannot be used with '%s'", schema.name) | |||
end | |||
|
|||
if schema.tags == true and options.tags ~= nil then | |||
if context ~= "select" then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does the options.tags
filter also work in generated methods for relationships, such as db.targets:each_for_upstream({ id = U.id }, nil, { tags = T })
? (i.e. "give me all targets of upstream X that have tag T") — there needs to be a test in the suite answering this question :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
each_for_* will not support tags because it will not parse "?tags=" from Admin API, and uses a different pager page_for_*
other than the tag-enabled function page
used by normal pagination. I'll add a unit test on each_for_upstream to ensure the functionality doesn't exist.
local helpers = require "spec.helpers" | ||
local cjson = require "cjson" | ||
|
||
-- We already test the functionality of page() when filtering by tag in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are no tests for /tags
?
- what happens with
/tags
with no arguments - what happens with
/tags/valid_tag
- what happens with
/tags/invalid@tag
- what happens when mixing
/tags
and?tags=
- what happens with
/tags/valid_tag?tags=another_tag
- what happens with
/tags/valid_tag?tags=another_tag,yet_another
- what happens with
/tags/valid_tag?tags=another_tag/yet_another
- what happens with
/tags/valid_tag?tags=invalid@tag
- what happens with
- anything else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tests for /tags and /tags/:tags in spec/02-integration/04-admin_api/14-tags_spec.lua 👍
@@ -487,6 +487,7 @@ for _, strategy in helpers.each_strategy() do | |||
regex_priority = 0, | |||
preserve_host = false, | |||
strip_path = true, | |||
tags = ngx.null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we return an empty set rather than null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will follow the same pattern as for example paths
for routes
, which if empty will be set to ngx.null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are different though: adding a path constricts the behaviour: it says "only consider this route when one of these array elements match" -> an empty path array would be effectively turning a route off.
Tags are different: an empty list of tags is the same as no tags, and so IMO should always be an array.
In practice this lets us remove lots of type checks, both inside of Kong, and in users of Kong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@james-callahan while I agree in principle that we should have been using empty sets rather than nulls everywhere (and I even disagree about your sub-point that paths are rightfully null, one could easily pick a semantics where empty set means no constraint added to paths), but this battle was fought in 2017 and lost (Ctrl+F "billion"), and now null is the default in all aggregate empty-accepting fields.
So, in short, @fffonion's design is fine, as it is consistent with the behavior of other attributes.
Since the changes that address the above comments are small and sparse, i'll make them a single commit. Once this PR looks good I'll squash them into previous commit for better commit history. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just reviewed the new commits and left a couple minor suggestions, but I'm happy with the current status of the PR. I think it looks good and once the latest changes made are squashed into their respective commits, I think it's a go.
kong/db/schema/entities/tags.lua
Outdated
@@ -7,7 +7,7 @@ return { | |||
dao = "kong.db.dao.tags", | |||
|
|||
fields = { | |||
{ tag = { type = "string", required = true, unique = false }, }, | |||
{ tag = { type = "string", required = true, unique = false, match = "^[%w%.%-%_~]+$" }, }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super minor suggestion so that the definition of a valid tag comes from a single source of truth:
{ tag = typedefs.tag { required = true } }
with a definition of tag
in typedefs.lua
below
kong/db/schema/typedefs.lua
Outdated
type = "string", | ||
match = "^[%w%.%-%_~]+$" | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typedefs.tag = Schema.define {
type = "string",
match = "^[%w%.%-%_~]+$"
}
typedefs.tags = Schema.define {
type = "set",
elements = typedefs.tag,
}
so you can reuse the definition of tag
in the schema of the tags
entity.
username = "adminapi-filter-by-tag-" .. i, | ||
tags = { "corp_a", "consumer"..i } | ||
} | ||
local row, err, err_t = bp.consumers:insert(consumer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor request that I'm sure will sound like it came out of nowhere: please insert items with bp
before you start_kong
. Besides making tests less dependent on event propagation in general, this actually will make the test compatible with declarative config! (I have a magic bp
in my branch that builds up a config.yml
to be used by start_kong
when using database=off
:) )
@@ -1,11 +1,27 @@ | |||
local Tags = {} | |||
|
|||
function Tags:page_by_tag(tag, size, offset, options) | |||
local ok, err = self.schema:validate_field(self.schema.fields.tag, tag) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
3e10256
to
a90ccd1
Compare
Hi, I have squashed commits. The tests are passing expect one https://travis-ci.org/Kong/kong/jobs/492317694, but I'm not sure how to understand this error. |
a90ccd1
to
9f69772
Compare
Review comments have been addressed
@fffonion Merged! \o/ |
This PR adds support for list entities by combination of tags, and listing all entity IDs with a specific tag.
Summary
Handling of tags per entity is done in different way depending on the strategy. For either strategy, a
tags
table is created to store tags for entities, each entity table also havetags
column.tags
column of an entity table is indexed with GIN index type:tags
column with Postgres array operator, which will hit index.tags
column of the table and update the columns intags
table as well. It uses trigger to ensure consistency.tags
table with correspondingentity_id
andentity_name
, then select from the entity table with the returned entity_ids.tags
column of the table and update the columns intags
table as well. It uses thecassandra:batch
function to ensure atomicity and consistency.For a custom entity that needs tags functionality, developer will need to set
tags
field to be true in the schema, add atags
field to fields defination, and add migrations as kong/db/migrations/core/003_100_to_110.lua.Full changelog
batch
function in the Cassandra connector.