Merge sidetag plugin

Originally living in https://pagure.io/sidetag-koji-plugin

Fixes: https://pagure.io/koji/issue/1955
This commit is contained in:
Tomas Kopecek 2020-01-20 15:33:56 +01:00
parent 8502d4d4ce
commit b9e0597ccd
4 changed files with 364 additions and 2 deletions

View file

@ -0,0 +1,90 @@
# Copyright © 2019 Red Hat, Inc.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from __future__ import absolute_import
from argparse import ArgumentParser
import koji
from koji.plugin import export_cli
from koji_cli.lib import _, activate_session, watch_tasks
from koji_cli.commands import anon_handle_wait_repo
@export_cli
def handle_add_sidetag(options, session, args):
"Create sidetag"
usage = _("usage: %(prog)s add-sidetag [options] <basetag>")
usage += _("\n(Specify the --help global option for a list of other help options)")
parser = ArgumentParser(usage=usage)
parser.add_argument("basetag", help="name of basetag")
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help=_("Do not print tag name"),
default=options.quiet,
)
parser.add_argument(
"-w", "--wait", action="store_true", help=_("Wait until repo is ready.")
)
opts = parser.parse_args(args)
activate_session(session, options)
try:
tag = session.createSideTag(basetag)
except koji.ActionNotAllowed:
parser.error(_("Policy violation"))
if not opts.quiet:
print (tag["name"])
if opts.wait:
args = ["--target", tag["name"]]
if opts.quiet:
args.append("--quiet")
anon_handle_wait_repo(options, session, args)
@export_cli
def handle_remove_sidetag(options, session, args):
"Remove sidetag"
usage = _("usage: %(prog)s remove-sidetag [options] <sidetag> ...")
usage += _("\n(Specify the --help global option for a list of other help options)")
parser = ArgumentParser(usage=usage)
parser.add_argument("sidetags", help="name of sidetag", nargs="+")
opts = parser.parse_args(args)
activate_session(session, options)
session.multicall = True
for sidetag in opts.sidetags:
session.removeSideTag(sidetag)
session.multiCall(strict=True)
@export_cli
def handle_list_sidetags(options, session, args):
"List sidetags"
usage = _("usage: %(prog)s list-sidetags [options]")
usage += _("\n(Specify the --help global option for a list of other help options)")
parser = ArgumentParser(usage=usage)
parser.add_argument("--basetag", action="store", help=_("Filter on basetag"))
parser.add_argument("--user", action="store", help=_("Filter on user"))
parser.add_argument("--mine", action="store_true", help=_("Filter on user"))
opts = parser.parse_args(args)
if opts.mine and opts.user:
parser.error(_("Specify only one from --user --mine"))
if opts.mine:
activate_session(session, options)
user = session.getLoggedInUser()["name"]
else:
user = opts.user
for tag in session.listSideTags(basetag=opts.basetag, user=user):
print (tag["name"])

3
plugins/hub/sidetag.conf Normal file
View file

@ -0,0 +1,3 @@
[sidetag]
# automatically remove sidetag on untagging last package
remove_empty = off

209
plugins/hub/sidetag_hub.py Normal file
View file

@ -0,0 +1,209 @@
# Copyright © 2019 Red Hat, Inc.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from koji.context import context
from koji.plugin import export, callback
import koji
import sys
CONFIG_FILE = "/etc/koji-hub/plugins/sidetag.conf"
CONFIG = None
sys.path.insert(0, "/usr/share/koji-hub/")
from kojihub import (
assert_policy,
get_tag,
get_user,
get_build_target,
_create_tag,
_create_build_target,
_delete_tag,
_delete_build_target,
readTaggedBuilds,
QueryProcessor,
nextval,
)
@export
def createSideTag(basetag):
"""Create a side tag.
:param basetag: name or ID of base tag
:type basetag: str or int
"""
# Any logged-in user is able to request creation of side tags,
# as long the request meets the policy.
context.session.assertLogin()
user = get_user(context.session.user_id, strict=True)
basetag = get_tag(basetag, strict=True)
query = QueryProcessor(
tables=["tag_extra"],
clauses=["key='sidetag_user_id'", "value=%(user_id)s", "active IS TRUE"],
columns=["COUNT(*)"],
aliases=["user_tags"],
values={"user_id": str(user["id"])},
)
user_tags = query.executeOne()
if user_tags is None:
# should not ever happen
raise koji.GenericError("Unknown db error")
# Policy is a very flexible mechanism, that can restrict for which
# tags sidetags can be created, or which users can create sidetags etc.
assert_policy(
"sidetag", {"tag": basetag["id"], "number_of_tags": user_tags["user_tags"]}
)
# ugly, it will waste one number in tag_id_seq, but result will match with
# id assigned by _create_tag
tag_id = nextval("tag_id_seq") + 1
sidetag_name = "%s-side-%s" % (basetag["name"], tag_id)
sidetag_id = _create_tag(
sidetag_name,
parent=basetag["id"],
arches=basetag["arches"],
extra={
"sidetag": True,
"sidetag_user": user["name"],
"sidetag_user_id": user["id"],
},
)
_create_build_target(sidetag_name, sidetag_id, sidetag_id)
return {"name": sidetag_name, "id": sidetag_id}
@export
def removeSideTag(sidetag):
"""Remove a side tag
:param sidetag: id or name of sidetag
:type sidetag: int or str
"""
context.session.assertLogin()
user = get_user(context.session.user_id, strict=True)
sidetag = get_tag(sidetag, strict=True)
# sanity/access
if not sidetag["extra"].get("sidetag"):
raise koji.GenericError("Not a sidetag: %(name)s" % sidetag)
if sidetag["extra"].get("sidetag_user_id") != user["id"]:
if not context.session.hasPerm("admin"):
raise koji.ActionNotAllowed("This is not your sidetag")
_remove_sidetag(sidetag)
def _remove_sidetag(sidetag):
# check target
target = get_build_target(sidetag["name"])
if not target:
raise koji.GenericError("Target is missing for sidetag")
if target["build_tag"] != sidetag["id"] or target["dest_tag"] != sidetag["id"]:
raise koji.GenericError("Target does not match sidetag")
_delete_build_target(target["id"])
_delete_tag(sidetag["id"])
@export
def listSideTags(basetag=None, user=None, queryOpts=None):
"""List all sidetags with additional filters
:param basetag: filter by basteag id or name
:type basetag: int or str
:param user: filter by userid or username
:type user: int or str
:param queryOpts: additional query options
{countOnly, order, offset, limit}
:type queryOpts: dict
"""
# te1.sidetag
# te2.user_id
# te3.basetag
if user is not None:
user_id = str(get_user(user, strict=True)["id"])
else:
user_id = None
if basetag is not None:
basetag_id = get_tag(basetag, strict=True)["id"]
else:
basetag_id = None
joins = ["LEFT JOIN tag_extra AS te1 ON tag.id = te1.tag_id"]
clauses = ["te1.active IS TRUE", "te1.key = 'sidetag'", "te1.value = 'true'"]
if user_id:
joins.append("LEFT JOIN tag_extra AS te2 ON tag.id = te2.tag_id")
clauses.extend(
[
"te2.active IS TRUE",
"te2.key = 'sidetag_user_id'",
"te2.value = %(user_id)s",
]
)
if basetag_id:
joins.append("LEFT JOIN tag_inheritance ON tag.id = tag_inheritance.tag_id")
clauses.extend(
[
"tag_inheritance.active IS TRUE",
"tag_inheritance.parent_id = %(basetag_id)s",
]
)
query = QueryProcessor(
tables=["tag"],
clauses=clauses,
columns=["tag.id", "tag.name"],
aliases=["id", "name"],
joins=joins,
values={"basetag_id": basetag_id, "user_id": user_id},
opts=queryOpts,
)
return query.execute()
def handle_sidetag_untag(cbtype, *args, **kws):
"""Remove a side tag when its last build is untagged
Note, that this is triggered only in case, that some build exists. For
never used tags, some other policy must be applied. Same holds for users
which don't untag their builds.
"""
if "tag" not in kws:
# shouldn't happen, but...
return
tag = get_tag(kws["tag"]["id"], strict=False)
if not tag:
# also shouldn't happen, but just in case
return
if not tag["extra"].get("sidetag"):
# not a side tag
return
# is the tag now empty?
query = QueryProcessor(
tables=["tag_listing"],
clauses=["tag_id = %(tag_id)s", "active IS TRUE"],
values={"tag_id": tag["id"]},
opts={"countOnly": True},
)
if query.execute():
return
# looks like we've just untagged the last build from a side tag
try:
# XXX: are we double updating tag_listing?
_remove_sidetag(tag)
except koji.GenericError:
pass
# read config and register
if not CONFIG:
CONFIG = koji.read_config_files(CONFIG_FILE)
if CONFIG.has_option("sidetag", "remove_empty") and CONFIG.getboolean(
"sidetag", "remove_empty"
):
handle_sidetag_untag = callback("postUntag")(handle_sidetag_untag)