Skip to content
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

Merged
merged 5 commits into from
Feb 13, 2019
Merged

feat(core) implement entity tagging #4275

merged 5 commits into from
Feb 13, 2019

Conversation

fffonion
Copy link
Contributor

@fffonion fffonion commented Feb 1, 2019

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 have tags column.

  • With Postgres, the tags column of an entity table is indexed with GIN index type:
    • When selecting entities by tags, the strategy queries the tags column with Postgres array operator, which will hit index.
    • When doing insert/update/upsert/delete, the strategy set the tags column of the table and update the columns in tags table as well. It uses trigger to ensure consistency.
  • With cassandra:
    • When selecting entities by tags, the strategy queries the tags table with corresponding entity_id and entity_name, then select from the entity table with the returned entity_ids.
    • When doing insert/update/upsert/delete, the strategy set the tags column of the table and update the columns in tags table as well. It uses the cassandra: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 a tags field to fields defination, and add migrations as kong/db/migrations/core/003_100_to_110.lua.

Full changelog

  • Implement storing tags for entities and listing entities by combination of tags.
  • Implement listing all tags or specific tag with related entity UUIDs.
  • Implement the batch function in the Cassandra connector.

bungle
bungle previously requested changes Feb 7, 2019
kong/api/endpoints.lua Outdated Show resolved Hide resolved
options.tags_cond = 'or'
options.tags = split(tags, '/')
else
options.tags = {}
Copy link
Member

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?

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"
Copy link
Member

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

kong/api/endpoints.lua Outdated Show resolved Hide resolved
kong/api/endpoints.lua Outdated Show resolved Hide resolved
kong/api/endpoints.lua Outdated Show resolved Hide resolved
Copy link
Contributor

@hishamhm hishamhm left a 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:

  1. Core:
    1.1. a pretty regular-looking tags attribute in core entity schemas (which triggers special behavior in the DAO when modified), which is currently injected by the magic tags = true
    1.2. a read-only Tags entity (which is only updated via side-effects on the tags attribute of other entities), used to support the page_by_tags
    1.3. new method options opts.tags and opts.tags_cond which filter on core entity searches
  2. 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 for id and that doesn't have special construction; the schema for Entities in kong.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 as schema.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 Show resolved Hide resolved
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"
Copy link
Contributor

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


local sql_templates = {
page_first = [[
SELECT entity_id, entity_name, tag, ordinarity
Copy link
Contributor

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()
Copy link
Contributor

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.

Copy link
Contributor Author

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

spec/02-integration/03-db/07-tags_spec.lua Outdated Show resolved Hide resolved
kong/api/endpoints.lua Outdated Show resolved Hide resolved
spec/02-integration/03-db/07-tags_spec.lua Outdated Show resolved Hide resolved
@@ -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
Copy link
Contributor

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 :)

Copy link
Contributor Author

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
Copy link
Contributor

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
  • anything else?

Copy link
Contributor Author

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 👍

kong/db/dao/tags.lua Outdated Show resolved Hide resolved
kong/db/strategies/postgres/init.lua Show resolved Hide resolved
kong/db/strategies/postgres/tags.lua Outdated Show resolved Hide resolved
@@ -487,6 +487,7 @@ for _, strategy in helpers.each_strategy() do
regex_priority = 0,
preserve_host = false,
strip_path = true,
tags = ngx.null,
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

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.

@fffonion
Copy link
Contributor Author

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.

Copy link
Contributor

@hishamhm hishamhm left a 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.

@@ -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%.%-%_~]+$" }, },
Copy link
Contributor

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

type = "string",
match = "^[%w%.%-%_~]+$"
},
}
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@fffonion fffonion force-pushed the feat/core-entity-tags branch 3 times, most recently from 3e10256 to a90ccd1 Compare February 12, 2019 19:47
@fffonion
Copy link
Contributor Author

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.

@hishamhm hishamhm force-pushed the feat/core-entity-tags branch from a90ccd1 to 9f69772 Compare February 12, 2019 23:43
@hishamhm hishamhm dismissed stale reviews from james-callahan and bungle February 13, 2019 12:18

Review comments have been addressed

@hishamhm hishamhm added this to the 1.1.0 milestone Feb 13, 2019
@hishamhm hishamhm merged commit abcfdf1 into next Feb 13, 2019
@hishamhm hishamhm deleted the feat/core-entity-tags branch February 13, 2019 12:28
@hishamhm
Copy link
Contributor

@fffonion Merged! \o/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants