[libvirt] [jenkins-ci PATCH v2 00/20] quayadmin: Introduce Quay API client

Changes from [v1]: * add Dan's R-b to the patch importing the initial implementation; * don't break quiet mode for show-repo, and make sure it works for all other show-* commands as well; * fix typos. [v1] https://www.redhat.com/archives/libvir-list/2019-July/msg01018.html Andrea Bolognani (20): quayadmin: Import initial implementation quayadmin: Fix argument parsing for delete-repo quayadmin: Tweak quiet logic quayadmin: Tweak show-repo output format quayadmin: Tweak list-repos output format quayadmin: Build endpoint, params and payload separately quayadmin: Implement debug mode quayadmin: Remove clientid and clientsecret quayadmin: Introduce configuration object quayadmin: Read configuration from file quayadmin: Add support for the PUT method quayadmin: Add list-triggers command quayadmin: Add show-trigger command quayadmin: Add activate-trigger command quayadmin: Add list-builds command quayadmin: Add show-build command quayadmin: Add list-tags command quayadmin: Add show-tag command quayadmin: Add create-tag command quayadmin: Add delete-tag command guests/quayadmin | 585 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100755 guests/quayadmin -- 2.21.0

This code was written by Daniel P. Berrangé, and contains the scaffolding necessary to call Quay APIs as well as the implementation of a few commands. Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 198 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 guests/quayadmin diff --git a/guests/quayadmin b/guests/quayadmin new file mode 100755 index 0000000..31ea929 --- /dev/null +++ b/guests/quayadmin @@ -0,0 +1,198 @@ +#!/usr/bin/python3 +# -*- python -*- +# +# quayadmin - client for quay.io +# +# Copyright (C) 2019 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. + +import argparse +import requests +import sys + +baseurl = "https://quay.io/api/v1" +clientid = "xxx" +clientsecret = "xxx" +token= "xxx" + + +def request(endpoint, method, payload=None, params=None): + url = baseurl + endpoint + + headers = { + "Authorization": "Bearer {}".format(token) + } + + return method(url, headers=headers, json=payload, params=params) + + +def get(endpoint, params=None): + return request(endpoint, method=requests.get, params=params) + + +def delete(endpoint, payload=None): + return request(endpoint, method=requests.delete, payload=payload) + + +def post(endpoint, payload=None): + return request(endpoint, method=requests.post, payload=payload) + + +def has_error(quiet, res, expected, message): + if res.status_code == expected: + return False + + if res.status_code >= 400 and res.status_code < 500: + info = res.json() + err = info["error_message"] + else: + err = res.content + + if not quiet: + print("{}: {} ({})".format(message, err, res.status_code)) + return True + + +def run_list_repos(args): + res = get("/repository", params={"namespace": args.namespace}) + + if has_error(args.quiet, res, 200, "Cannot list repositories"): + return 1 + + info = res.json() + for repo in info["repositories"]: + print ("{}/{}".format(repo["namespace"], repo["name"])) + + +def run_show_repo(args): + res = get("/repository/{}/{}".format(args.namespace, args.repo)) + + if has_error(args.quiet, res, 200, "Cannot query repository {}/{}" + .format(args.namespace, args.repo)): + return 1 + + info = res.json() + if not args.quiet: + print("{}/{}: {}".format(args.namespace, args.repo, info["description"])) + + +def run_create_repo(args): + res = post("/repository", payload={ + "repo_kind": "image", + "namespace": args.namespace, + "visibility": "public", + "repository": args.repo, + "description": args.desc, + }) + + if has_error(args.quiet, res, 201, "Cannot create repository {}/{}" + .format(args.namespace, args.repo)): + return 1 + + if not args.quiet: + print("Repository {}/{} created".format(args.namespace, args.repo)) + + +def run_delete_repo(args): + res = delete("/repository/{}/{}".format(args.namespace, args.repo)) + + if has_error(args.quiet, res, 204, "Cannot delete repository {}/{}" + .format(args.namespace, args.repo)): + return 1 + + if not args.quiet: + print("Repository {}/{} deleted".format(args.namespace, args.repo)) + + +def add_arg_namespace(parser): + parser.add_argument("namespace", help="Organization or user name") + + +def add_arg_repo(parser): + parser.add_argument("repo", help="Repository name") + + +def add_arg_desc(parser): + parser.add_argument("desc", help="Repository description") + + +def build_parser_list_repos(subparser): + parser = subparser.add_parser("list-repos", help="List container repositories") + + parser.set_defaults(func=run_list_repos) + + add_arg_namespace(parser) + + +def build_parser_create_repo(subparser): + parser = subparser.add_parser("create-repo", help="Create a new repository") + + parser.set_defaults(func=run_create_repo) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_desc(parser) + + +def build_parser_show_repo(subparser): + parser = subparser.add_parser("show-repo", help="Show repository info") + + parser.set_defaults(func=run_show_repo) + + add_arg_namespace(parser) + add_arg_repo(parser) + + +def build_parser_delete_repo(subparser): + parser = subparser.add_parser("delete-repo", help="Delete an existing repository") + + parser.set_defaults(func=run_create_repo) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_desc(parser) + + +def build_parser(): + parser = argparse.ArgumentParser( + description="quay.io client admin tool" + ) + + parser.add_argument("--debug", '-d', action="store_true", help="Print debugging information") + parser.add_argument("--quiet", '-q', action="store_true", help="Display minimal information") + + subparser = parser.add_subparsers(metavar="COMMAND") + subparser.required = True + + build_parser_list_repos(subparser) + build_parser_show_repo(subparser) + build_parser_create_repo(subparser) + build_parser_delete_repo(subparser) + + return parser + +def main(): + parser = build_parser() + args = parser.parse_args() + + try: + res = args.func(args) + sys.exit(res) + except Exception as ex: + sys.stderr.write("{}: {}\n".format(sys.argv[0], ex)) + sys.exit(1) + +if __name__ == "__main__": + main() -- 2.21.0

It was clearly copied from create-repo and not cleaned up sufficiently afterwards. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guests/quayadmin b/guests/quayadmin index 31ea929..5dc5eff 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -158,11 +158,10 @@ def build_parser_show_repo(subparser): def build_parser_delete_repo(subparser): parser = subparser.add_parser("delete-repo", help="Delete an existing repository") - parser.set_defaults(func=run_create_repo) + parser.set_defaults(func=run_delete_repo) add_arg_namespace(parser) add_arg_repo(parser) - add_arg_desc(parser) def build_parser(): -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:11PM +0200, Andrea Bolognani wrote:
It was clearly copied from create-repo and not cleaned up sufficiently afterwards.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

If we've been asked not to produce any output, we can bail early: doing so means we don't need to increase indentation for subsequent code, and in some cases we can even avoid fetching the JSON data from the response object. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/guests/quayadmin b/guests/quayadmin index 5dc5eff..3e6cc87 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -83,9 +83,12 @@ def run_show_repo(args): .format(args.namespace, args.repo)): return 1 + if args.quiet: + return 0 + info = res.json() - if not args.quiet: - print("{}/{}: {}".format(args.namespace, args.repo, info["description"])) + + print("{}/{}: {}".format(args.namespace, args.repo, info["description"])) def run_create_repo(args): @@ -101,8 +104,10 @@ def run_create_repo(args): .format(args.namespace, args.repo)): return 1 - if not args.quiet: - print("Repository {}/{} created".format(args.namespace, args.repo)) + if args.quiet: + return 0 + + print("Repository {}/{} created".format(args.namespace, args.repo)) def run_delete_repo(args): @@ -112,8 +117,10 @@ def run_delete_repo(args): .format(args.namespace, args.repo)): return 1 - if not args.quiet: - print("Repository {}/{} deleted".format(args.namespace, args.repo)) + if args.quiet: + return 0 + + print("Repository {}/{} deleted".format(args.namespace, args.repo)) def add_arg_namespace(parser): -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:12PM +0200, Andrea Bolognani wrote:
If we've been asked not to produce any output, we can bail early: doing so means we don't need to increase indentation for subsequent code, and in some cases we can even avoid fetching the JSON data from the response object.
Unless I'm mis-reading the last point doesn't seem to affect this patch - we're still fetching JSON, which is good I think, as it means we check the response is well formed, and not an error of some kind
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
diff --git a/guests/quayadmin b/guests/quayadmin index 5dc5eff..3e6cc87 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -83,9 +83,12 @@ def run_show_repo(args): .format(args.namespace, args.repo)): return 1
+ if args.quiet: + return 0 + info = res.json() - if not args.quiet: - print("{}/{}: {}".format(args.namespace, args.repo, info["description"])) + + print("{}/{}: {}".format(args.namespace, args.repo, info["description"]))
def run_create_repo(args): @@ -101,8 +104,10 @@ def run_create_repo(args): .format(args.namespace, args.repo)): return 1
- if not args.quiet: - print("Repository {}/{} created".format(args.namespace, args.repo)) + if args.quiet: + return 0 + + print("Repository {}/{} created".format(args.namespace, args.repo))
def run_delete_repo(args): @@ -112,8 +117,10 @@ def run_delete_repo(args): .format(args.namespace, args.repo)): return 1
- if not args.quiet: - print("Repository {}/{} deleted".format(args.namespace, args.repo)) + if args.quiet: + return 0 + + print("Repository {}/{} deleted".format(args.namespace, args.repo))
def add_arg_namespace(parser): -- 2.21.0
-- libvir-list mailing list libvir-list@redhat.com https://www.redhat.com/mailman/listinfo/libvir-list
Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

On Wed, 2019-07-17 at 18:14 +0100, Daniel P. Berrangé wrote:
On Wed, Jul 17, 2019 at 03:49:12PM +0200, Andrea Bolognani wrote:
If we've been asked not to produce any output, we can bail early: doing so means we don't need to increase indentation for subsequent code, and in some cases we can even avoid fetching the JSON data from the response object.
Unless I'm mis-reading the last point doesn't seem to affect this patch - we're still fetching JSON, which is good I think, as it means we check the response is well formed, and not an error of some kind
We skip calling res.json() when all we need the JSON for is some data to be displayed and we're in quiet mode. All the usual checks on the return code still happens. I assume res.json() needs to do some processing when it's called, and now we can skip that. Either way that's a secondary concern, it was mostly about the indentation :) Thanks for the review, and for getting the script started in the first place! The entire series has been pushed now. -- Andrea Bolognani / Red Hat / Virtualization

We're going to introduce several other show-* commands later, and we want their output to be more or less consistent, so start by tweaking the existing command. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/guests/quayadmin b/guests/quayadmin index 3e6cc87..536fbaf 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -88,7 +88,10 @@ def run_show_repo(args): info = res.json() - print("{}/{}: {}".format(args.namespace, args.repo, info["description"])) + print("repo:") + print(" namespace: {}".format(args.namespace)) + print(" repo: {}".format(args.repo)) + print(" description: {}".format(info["description"])) def run_create_repo(args): -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:13PM +0200, Andrea Bolognani wrote:
We're going to introduce several other show-* commands later, and we want their output to be more or less consistent, so start by tweaking the existing command.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

We're going to introduce several other list-* commands later, and we want their output to be more or less consistent, so start by tweaking the existing command. More specifically, we want the output of each list-* command to be such that you can take one of the lines, append it to the arguments you just used and by doing so obtain a valid list of arguments for the corresponding show-* command, eg. $ quayadmin list-repos libvirt buildenv-debian-10 $ quayadmin show-repo libvirt buildenv-debian-10 repo: namespace: libvirt repo: buildenv-debian-10 $ Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guests/quayadmin b/guests/quayadmin index 536fbaf..6eeacd1 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -73,7 +73,7 @@ def run_list_repos(args): info = res.json() for repo in info["repositories"]: - print ("{}/{}".format(repo["namespace"], repo["name"])) + print ("{}".format(repo["name"])) def run_show_repo(args): -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:14PM +0200, Andrea Bolognani wrote:
We're going to introduce several other list-* commands later, and we want their output to be more or less consistent, so start by tweaking the existing command.
More specifically, we want the output of each list-* command to be such that you can take one of the lines, append it to the arguments you just used and by doing so obtain a valid list of arguments for the corresponding show-* command, eg.
$ quayadmin list-repos libvirt buildenv-debian-10 $ quayadmin show-repo libvirt buildenv-debian-10 repo: namespace: libvirt repo: buildenv-debian-10 $
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Instead of building all arguments for a request at the same time as the request itself is prepared, build them beforehand and then pass them to the corresponding function. This makes the code more readable, especially when complex params or long endpoints are involved. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/guests/quayadmin b/guests/quayadmin index 6eeacd1..7ddf7a4 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -66,18 +66,26 @@ def has_error(quiet, res, expected, message): def run_list_repos(args): - res = get("/repository", params={"namespace": args.namespace}) + endpoint = "/repository" + params = { + "namespace": args.namespace, + } + + res = get(endpoint, params=params) if has_error(args.quiet, res, 200, "Cannot list repositories"): return 1 info = res.json() + for repo in info["repositories"]: print ("{}".format(repo["name"])) def run_show_repo(args): - res = get("/repository/{}/{}".format(args.namespace, args.repo)) + endpoint = "/repository/{}/{}".format(args.namespace, args.repo) + + res = get(endpoint) if has_error(args.quiet, res, 200, "Cannot query repository {}/{}" .format(args.namespace, args.repo)): @@ -95,13 +103,16 @@ def run_show_repo(args): def run_create_repo(args): - res = post("/repository", payload={ + endpoint = "/repository" + payload = { "repo_kind": "image", "namespace": args.namespace, "visibility": "public", "repository": args.repo, "description": args.desc, - }) + } + + res = post(endpoint, payload=payload) if has_error(args.quiet, res, 201, "Cannot create repository {}/{}" .format(args.namespace, args.repo)): @@ -114,7 +125,9 @@ def run_create_repo(args): def run_delete_repo(args): - res = delete("/repository/{}/{}".format(args.namespace, args.repo)) + endpoint = "/repository/{}/{}".format(args.namespace, args.repo) + + res = delete(endpoint) if has_error(args.quiet, res, 204, "Cannot delete repository {}/{}" .format(args.namespace, args.repo)): -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:15PM +0200, Andrea Bolognani wrote:
Instead of building all arguments for a request at the same time as the request itself is prepared, build them beforehand and then pass them to the corresponding function.
This makes the code more readable, especially when complex params or long endpoints are involved.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

While a command line parameter for enabling debug mode has been defined, it's not currently used in any form or shape. Make it so enabling debug mode actually does something useful, that is, prints the most important parts of the conversation happening between the client and the server. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/guests/quayadmin b/guests/quayadmin index 7ddf7a4..04a8b66 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -19,6 +19,7 @@ # with this program. If not, see <https://www.gnu.org/licenses/>. import argparse +import pprint import requests import sys @@ -28,26 +29,42 @@ clientsecret = "xxx" token= "xxx" -def request(endpoint, method, payload=None, params=None): +def request(endpoint, method, payload=None, params=None, debug=False): url = baseurl + endpoint headers = { "Authorization": "Bearer {}".format(token) } - return method(url, headers=headers, json=payload, params=params) + if debug: + print("=> {") + print(" url={}".format(url)) + print(" params={}".format(pprint.pformat(params))) + print(" payload={}".format(pprint.pformat(payload))) + print("}") + res = method(url, headers=headers, json=payload, params=params) -def get(endpoint, params=None): - return request(endpoint, method=requests.get, params=params) + if debug: + print("<= {") + print(" status_code={}".format(res.status_code)) + if res.text is not None and len(res.text) > 0: + print(" json={}".format(pprint.pformat(res.json()))) + print("}") + return res -def delete(endpoint, payload=None): - return request(endpoint, method=requests.delete, payload=payload) +def get(endpoint, params=None, debug=False): + return request(endpoint, method=requests.get, params=params, debug=debug) -def post(endpoint, payload=None): - return request(endpoint, method=requests.post, payload=payload) + +def delete(endpoint, payload=None, debug=False): + return request(endpoint, method=requests.delete, payload=payload, debug=debug) + + +def post(endpoint, payload=None, debug=False): + return request(endpoint, method=requests.post, payload=payload, debug=debug) def has_error(quiet, res, expected, message): @@ -71,7 +88,7 @@ def run_list_repos(args): "namespace": args.namespace, } - res = get(endpoint, params=params) + res = get(endpoint, params=params, debug=args.debug) if has_error(args.quiet, res, 200, "Cannot list repositories"): return 1 @@ -85,7 +102,7 @@ def run_list_repos(args): def run_show_repo(args): endpoint = "/repository/{}/{}".format(args.namespace, args.repo) - res = get(endpoint) + res = get(endpoint, debug=args.debug) if has_error(args.quiet, res, 200, "Cannot query repository {}/{}" .format(args.namespace, args.repo)): @@ -112,7 +129,7 @@ def run_create_repo(args): "description": args.desc, } - res = post(endpoint, payload=payload) + res = post(endpoint, payload=payload, debug=args.debug) if has_error(args.quiet, res, 201, "Cannot create repository {}/{}" .format(args.namespace, args.repo)): @@ -127,7 +144,7 @@ def run_create_repo(args): def run_delete_repo(args): endpoint = "/repository/{}/{}".format(args.namespace, args.repo) - res = delete(endpoint) + res = delete(endpoint, debug=args.debug) if has_error(args.quiet, res, 204, "Cannot delete repository {}/{}" .format(args.namespace, args.repo)): -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:16PM +0200, Andrea Bolognani wrote:
While a command line parameter for enabling debug mode has been defined, it's not currently used in any form or shape.
Make it so enabling debug mode actually does something useful, that is, prints the most important parts of the conversation happening between the client and the server.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

They're not currently used, and don't look like they're actually necessary to use the Quay API. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 2 -- 1 file changed, 2 deletions(-) diff --git a/guests/quayadmin b/guests/quayadmin index 04a8b66..218a945 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -24,8 +24,6 @@ import requests import sys baseurl = "https://quay.io/api/v1" -clientid = "xxx" -clientsecret = "xxx" token= "xxx" -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:17PM +0200, Andrea Bolognani wrote:
They're not currently used, and don't look like they're actually necessary to use the Quay API.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 2 -- 1 file changed, 2 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Instead of storing the client configuration in global variables, create a single configuration object to store all of them and pass around for functions to use. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/guests/quayadmin b/guests/quayadmin index 218a945..67873aa 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -23,15 +23,20 @@ import pprint import requests import sys -baseurl = "https://quay.io/api/v1" -token= "xxx" +def get_config(): + config = { + "baseurl": "https://quay.io/api/v1", + "token": "xxx", + } + + return config -def request(endpoint, method, payload=None, params=None, debug=False): - url = baseurl + endpoint +def request(config, endpoint, method, payload=None, params=None, debug=False): + url = config["baseurl"] + endpoint headers = { - "Authorization": "Bearer {}".format(token) + "Authorization": "Bearer {}".format(config["token"]) } if debug: @@ -53,16 +58,16 @@ def request(endpoint, method, payload=None, params=None, debug=False): return res -def get(endpoint, params=None, debug=False): - return request(endpoint, method=requests.get, params=params, debug=debug) +def get(config, endpoint, params=None, debug=False): + return request(config, endpoint, method=requests.get, params=params, debug=debug) -def delete(endpoint, payload=None, debug=False): - return request(endpoint, method=requests.delete, payload=payload, debug=debug) +def delete(config, endpoint, payload=None, debug=False): + return request(config, endpoint, method=requests.delete, payload=payload, debug=debug) -def post(endpoint, payload=None, debug=False): - return request(endpoint, method=requests.post, payload=payload, debug=debug) +def post(config, endpoint, payload=None, debug=False): + return request(config, endpoint, method=requests.post, payload=payload, debug=debug) def has_error(quiet, res, expected, message): @@ -80,13 +85,13 @@ def has_error(quiet, res, expected, message): return True -def run_list_repos(args): +def run_list_repos(config, args): endpoint = "/repository" params = { "namespace": args.namespace, } - res = get(endpoint, params=params, debug=args.debug) + res = get(config, endpoint, params=params, debug=args.debug) if has_error(args.quiet, res, 200, "Cannot list repositories"): return 1 @@ -97,10 +102,10 @@ def run_list_repos(args): print ("{}".format(repo["name"])) -def run_show_repo(args): +def run_show_repo(config, args): endpoint = "/repository/{}/{}".format(args.namespace, args.repo) - res = get(endpoint, debug=args.debug) + res = get(config, endpoint, debug=args.debug) if has_error(args.quiet, res, 200, "Cannot query repository {}/{}" .format(args.namespace, args.repo)): @@ -117,7 +122,7 @@ def run_show_repo(args): print(" description: {}".format(info["description"])) -def run_create_repo(args): +def run_create_repo(config, args): endpoint = "/repository" payload = { "repo_kind": "image", @@ -127,7 +132,7 @@ def run_create_repo(args): "description": args.desc, } - res = post(endpoint, payload=payload, debug=args.debug) + res = post(config, endpoint, payload=payload, debug=args.debug) if has_error(args.quiet, res, 201, "Cannot create repository {}/{}" .format(args.namespace, args.repo)): @@ -139,10 +144,10 @@ def run_create_repo(args): print("Repository {}/{} created".format(args.namespace, args.repo)) -def run_delete_repo(args): +def run_delete_repo(config, args): endpoint = "/repository/{}/{}".format(args.namespace, args.repo) - res = delete(endpoint, debug=args.debug) + res = delete(config, endpoint, debug=args.debug) if has_error(args.quiet, res, 204, "Cannot delete repository {}/{}" .format(args.namespace, args.repo)): @@ -225,7 +230,7 @@ def main(): args = parser.parse_args() try: - res = args.func(args) + res = args.func(get_config(), args) sys.exit(res) except Exception as ex: sys.stderr.write("{}: {}\n".format(sys.argv[0], ex)) -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:18PM +0200, Andrea Bolognani wrote:
Instead of storing the client configuration in global variables, create a single configuration object to store all of them and pass around for functions to use.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

We don't want sensitive information such as the API token to be stored into the script, both because it could lead to them being leaked by mistake and because it makes it needlessly complicated for users to take advantage of the tool. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/guests/quayadmin b/guests/quayadmin index 67873aa..5db7895 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -19,15 +19,32 @@ # with this program. If not, see <https://www.gnu.org/licenses/>. import argparse +import configparser +import os import pprint import requests import sys def get_config(): - config = { - "baseurl": "https://quay.io/api/v1", - "token": "xxx", - } + try: + path = os.environ["XDG_CONFIG_HOME"] + except KeyError: + path = os.path.join(os.environ["HOME"], ".config") + path = os.path.join(os.path.join(path, "quayadmin"), "config.ini") + + try: + parser = configparser.ConfigParser() + parser.read_file(open(path)) + except Exception as ex: + raise Exception("Cannot load config: {}".format(ex)) + + try: + config = { + "baseurl": "https://quay.io/api/v1", + "token": parser["DEFAULT"]["token"], + } + except KeyError: + raise Exception("Token not found in {}".format(path)) return config -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:19PM +0200, Andrea Bolognani wrote:
We don't want sensitive information such as the API token to be stored into the script, both because it could lead to them being leaked by mistake and because it makes it needlessly complicated for users to take advantage of the tool.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

We're going to need it for some specific Quay APIs later on. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index 5db7895..bbb35a2 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -87,6 +87,10 @@ def post(config, endpoint, payload=None, debug=False): return request(config, endpoint, method=requests.post, payload=payload, debug=debug) +def put(config, endpoint, payload=None, debug=False): + return request(config, endpoint, method=requests.put, payload=payload, debug=debug) + + def has_error(quiet, res, expected, message): if res.status_code == expected: return False -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:20PM +0200, Andrea Bolognani wrote:
We're going to need it for some specific Quay APIs later on.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 4 ++++ 1 file changed, 4 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index bbb35a2..99c7a27 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -180,6 +180,22 @@ def run_delete_repo(config, args): print("Repository {}/{} deleted".format(args.namespace, args.repo)) +def run_list_triggers(config, args): + endpoint = "/repository/{}/{}/trigger".format(args.namespace, args.repo) + + res = get(config, endpoint, debug=args.debug) + + if has_error(args.quiet, res, 200, + "Cannot list triggers for repository {}/{}" + .format(args.namespace, args.repo)): + return 1 + + info = res.json() + + for trigger in info["triggers"]: + print(trigger["id"]) + + def add_arg_namespace(parser): parser.add_argument("namespace", help="Organization or user name") @@ -228,6 +244,15 @@ def build_parser_delete_repo(subparser): add_arg_repo(parser) +def build_parser_list_triggers(subparser): + parser = subparser.add_parser("list-triggers", help="List repository triggers") + + parser.set_defaults(func=run_list_triggers) + + add_arg_namespace(parser) + add_arg_repo(parser) + + def build_parser(): parser = argparse.ArgumentParser( description="quay.io client admin tool" @@ -244,6 +269,8 @@ def build_parser(): build_parser_create_repo(subparser) build_parser_delete_repo(subparser) + build_parser_list_triggers(subparser) + return parser def main(): -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:21PM +0200, Andrea Bolognani wrote:
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index 99c7a27..cd989b3 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -196,6 +196,31 @@ def run_list_triggers(config, args): print(trigger["id"]) +def run_show_trigger(config, args): + endpoint = "/repository/{}/{}/trigger/{}".format(args.namespace, + args.repo, args.trigger) + + res = get(config, endpoint, debug=args.debug) + + if has_error(args.quiet, res, 200, + "Cannot query trigger {} for repository {}/{}" + .format(args.trigger, args.namespace, args.repo)): + return 1 + + if args.quiet: + return 0 + + info = res.json() + + print("trigger:") + print(" namespace: {}".format(args.namespace)) + print(" repo: {}".format(args.repo)) + print(" id: {}".format(args.trigger)) + print(" source_repo: {}".format(info["config"]["build_source"])) + print(" source_path: {}".format(info["config"]["dockerfile_path"])) + print(" activatable: {}".format(info["can_invoke"])) + + def add_arg_namespace(parser): parser.add_argument("namespace", help="Organization or user name") @@ -208,6 +233,10 @@ def add_arg_desc(parser): parser.add_argument("desc", help="Repository description") +def add_arg_trigger(parser): + parser.add_argument("trigger", help="Trigger ID") + + def build_parser_list_repos(subparser): parser = subparser.add_parser("list-repos", help="List container repositories") @@ -253,6 +282,16 @@ def build_parser_list_triggers(subparser): add_arg_repo(parser) +def build_parser_show_trigger(subparser): + parser = subparser.add_parser("show-trigger", help="Show trigger information") + + parser.set_defaults(func=run_show_trigger) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_trigger(parser) + + def build_parser(): parser = argparse.ArgumentParser( description="quay.io client admin tool" @@ -270,6 +309,7 @@ def build_parser(): build_parser_delete_repo(subparser) build_parser_list_triggers(subparser) + build_parser_show_trigger(subparser) return parser -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:22PM +0200, Andrea Bolognani wrote:
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index cd989b3..465cd8d 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -221,6 +221,30 @@ def run_show_trigger(config, args): print(" activatable: {}".format(info["can_invoke"])) +def run_activate_trigger(config, args): + endpoint = "/repository/{}/{}/trigger/{}/start".format(args.namespace, + args.repo, + args.trigger) + payload = { + "commit_sha": args.commit, + } + + res = post(config, endpoint, payload=payload, debug=args.debug) + + if has_error(args.quiet, res, 201, + "Cannot activate trigger {} for repository {}/{}" + .format(args.trigger, args.namespace, args.repo)): + return 1 + + if args.quiet: + return 0 + + info = res.json() + + print("Build {} for {}/{} created".format(info["id"], + args.namespace, args.repo)) + + def add_arg_namespace(parser): parser.add_argument("namespace", help="Organization or user name") @@ -237,6 +261,10 @@ def add_arg_trigger(parser): parser.add_argument("trigger", help="Trigger ID") +def add_arg_commit(parser): + parser.add_argument("commit", help="Git commit hash") + + def build_parser_list_repos(subparser): parser = subparser.add_parser("list-repos", help="List container repositories") @@ -292,6 +320,17 @@ def build_parser_show_trigger(subparser): add_arg_trigger(parser) +def build_parser_activate_trigger(subparser): + parser = subparser.add_parser("activate-trigger", help="Start build from trigger") + + parser.set_defaults(func=run_activate_trigger) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_trigger(parser) + add_arg_commit(parser) + + def build_parser(): parser = argparse.ArgumentParser( description="quay.io client admin tool" @@ -310,6 +349,7 @@ def build_parser(): build_parser_list_triggers(subparser) build_parser_show_trigger(subparser) + build_parser_activate_trigger(subparser) return parser -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:23PM +0200, Andrea Bolognani wrote:
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index 465cd8d..c2ea2b7 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -180,6 +180,22 @@ def run_delete_repo(config, args): print("Repository {}/{} deleted".format(args.namespace, args.repo)) +def run_list_builds(config, args): + endpoint = "/repository/{}/{}/build".format(args.namespace, args.repo) + + res = get(config, endpoint, debug=args.debug) + + if has_error(args.quiet, res, 200, + "Cannot list builds for repository {}/{}" + .format(args.namespace, args.repo)): + return 1 + + info = res.json() + + for build in info["builds"]: + print(build["id"]) + + def run_list_triggers(config, args): endpoint = "/repository/{}/{}/trigger".format(args.namespace, args.repo) @@ -301,6 +317,15 @@ def build_parser_delete_repo(subparser): add_arg_repo(parser) +def build_parser_list_builds(subparser): + parser = subparser.add_parser("list-builds", help="List repository builds") + + parser.set_defaults(func=run_list_builds) + + add_arg_namespace(parser) + add_arg_repo(parser) + + def build_parser_list_triggers(subparser): parser = subparser.add_parser("list-triggers", help="List repository triggers") @@ -347,6 +372,8 @@ def build_parser(): build_parser_create_repo(subparser) build_parser_delete_repo(subparser) + build_parser_list_builds(subparser) + build_parser_list_triggers(subparser) build_parser_show_trigger(subparser) build_parser_activate_trigger(subparser) -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:24PM +0200, Andrea Bolognani wrote:
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index c2ea2b7..f71ad2d 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -196,6 +196,30 @@ def run_list_builds(config, args): print(build["id"]) +def run_show_build(config, args): + endpoint = "/repository/{}/{}/build/{}".format(args.namespace, + args.repo, args.build) + + res = get(config, endpoint, debug=args.debug) + + if has_error(args.quiet, res, 200, + "Cannot query build {} for repository {}/{}" + .format(args.build, args.namespace, args.repo)): + return 1 + + if args.quiet: + return 0 + + info = res.json() + + print("build:") + print(" namespace: {}".format(args.namespace)) + print(" repo: {}".format(args.repo)) + print(" id: {}".format(args.build)) + print(" phase: {}".format(info["phase"])) + print(" started: {}".format(info["started"])) + + def run_list_triggers(config, args): endpoint = "/repository/{}/{}/trigger".format(args.namespace, args.repo) @@ -281,6 +305,10 @@ def add_arg_commit(parser): parser.add_argument("commit", help="Git commit hash") +def add_arg_build(parser): + parser.add_argument("build", help="Build ID") + + def build_parser_list_repos(subparser): parser = subparser.add_parser("list-repos", help="List container repositories") @@ -326,6 +354,16 @@ def build_parser_list_builds(subparser): add_arg_repo(parser) +def build_parser_show_build(subparser): + parser = subparser.add_parser("show-build", help="Show build info") + + parser.set_defaults(func=run_show_build) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_build(parser) + + def build_parser_list_triggers(subparser): parser = subparser.add_parser("list-triggers", help="List repository triggers") @@ -373,6 +411,7 @@ def build_parser(): build_parser_delete_repo(subparser) build_parser_list_builds(subparser) + build_parser_show_build(subparser) build_parser_list_triggers(subparser) build_parser_show_trigger(subparser) -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:25PM +0200, Andrea Bolognani wrote:
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

The implementation is limited in that it will not work properly if a repository contains more than 100 tags, but that's not really a big concern for us as each repository is only going to contain a single tag (latest). Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index f71ad2d..aa897f9 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -180,6 +180,26 @@ def run_delete_repo(config, args): print("Repository {}/{} deleted".format(args.namespace, args.repo)) +def run_list_tags(config, args): + endpoint = "/repository/{}/{}/tag".format(args.namespace, args.repo) + params = { + "onlyActiveTags": True, + "limit": 100, + } + + res = get(config, endpoint, params=params, debug=args.debug) + + if has_error(args.quiet, res, 200, + "Cannot list tags for repository {}/{}" + .format(args.namespace, args.repo)): + return 1 + + info = res.json() + + for tag in info["tags"]: + print ("{}".format(tag["name"])) + + def run_list_builds(config, args): endpoint = "/repository/{}/{}/build".format(args.namespace, args.repo) @@ -345,6 +365,15 @@ def build_parser_delete_repo(subparser): add_arg_repo(parser) +def build_parser_list_tags(subparser): + parser = subparser.add_parser("list-tags", help="List repository tags") + + parser.set_defaults(func=run_list_tags) + + add_arg_namespace(parser) + add_arg_repo(parser) + + def build_parser_list_builds(subparser): parser = subparser.add_parser("list-builds", help="List repository builds") @@ -410,6 +439,8 @@ def build_parser(): build_parser_create_repo(subparser) build_parser_delete_repo(subparser) + build_parser_list_tags(subparser) + build_parser_list_builds(subparser) build_parser_show_build(subparser) -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:26PM +0200, Andrea Bolognani wrote:
The implementation is limited in that it will not work properly if a repository contains more than 100 tags, but that's not really a big concern for us as each repository is only going to contain a single tag (latest).
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

The implementation is a bit awkward and surprising because the GET /api/v1/repository/{repository}/tag/{tag}/images API does not report the information we're looking form in an easy to consume form. It also suffers from the same limitations as the list-tags command. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index aa897f9..f99ac03 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -200,6 +200,43 @@ def run_list_tags(config, args): print ("{}".format(tag["name"])) +def run_show_tag(config, args): + endpoint = "/repository/{}/{}/tag".format(args.namespace, args.repo) + params = { + "onlyActiveTags": True, + "limit": 100, + } + + res = get(config, endpoint, params=params, debug=args.debug) + + if has_error(args.quiet, res, 200, + "Cannot list tags for repository {}/{}" + .format(args.namespace, args.repo)): + return 1 + + info = res.json() + + image = None + for tag in info["tags"]: + if tag["name"] == args.tag: + image = tag["image_id"] + break + + if image is None: + print("Cannot query tag {} for repository {}/{}: Not Found (404)" + .format(args.tag, args.namespace, args.repo)) + return 1 + + if args.quiet: + return 0 + + print("tag:") + print(" namespace: {}".format(args.namespace)) + print(" repo: {}".format(args.repo)) + print(" id: {}".format(args.tag)) + print(" image: {}".format(image)) + + def run_list_builds(config, args): endpoint = "/repository/{}/{}/build".format(args.namespace, args.repo) @@ -329,6 +366,10 @@ def add_arg_build(parser): parser.add_argument("build", help="Build ID") +def add_arg_tag(parser): + parser.add_argument("tag", help="Tag ID") + + def build_parser_list_repos(subparser): parser = subparser.add_parser("list-repos", help="List container repositories") @@ -374,6 +415,16 @@ def build_parser_list_tags(subparser): add_arg_repo(parser) +def build_parser_show_tag(subparser): + parser = subparser.add_parser("show-tag", help="Show tag information") + + parser.set_defaults(func=run_show_tag) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_tag(parser) + + def build_parser_list_builds(subparser): parser = subparser.add_parser("list-builds", help="List repository builds") @@ -440,6 +491,7 @@ def build_parser(): build_parser_delete_repo(subparser) build_parser_list_tags(subparser) + build_parser_show_tag(subparser) build_parser_list_builds(subparser) build_parser_show_build(subparser) -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:27PM +0200, Andrea Bolognani wrote:
The implementation is a bit awkward and surprising because the
GET /api/v1/repository/{repository}/tag/{tag}/images
API does not report the information we're looking form in an easy to consume form. It also suffers from the same limitations as the list-tags command.
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index f99ac03..a35996e 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -237,6 +237,28 @@ def run_show_tag(config, args): print(" image: {}".format(image)) +def run_create_tag(config, args): + endpoint = "/repository/{}/{}/tag/{}".format(args.namespace, args.repo, + args.tag) + payload = { + "image": args.image, + } + + res = put(config, endpoint, payload=payload, debug=args.debug) + + if has_error(args.quiet, res, 201, + "Cannot create tag {} for repository {}/{}" + .format(args.tag, args.namespace, args.repo)): + return 1 + + if args.quiet: + return 0 + + print("Tag {} created in repository {}/{}".format(args.tag, + args.namespace, + args.repo)) + + def run_list_builds(config, args): endpoint = "/repository/{}/{}/build".format(args.namespace, args.repo) @@ -370,6 +392,10 @@ def add_arg_tag(parser): parser.add_argument("tag", help="Tag ID") +def add_arg_image(parser): + parser.add_argument("image", help="Image ID") + + def build_parser_list_repos(subparser): parser = subparser.add_parser("list-repos", help="List container repositories") @@ -425,6 +451,17 @@ def build_parser_show_tag(subparser): add_arg_tag(parser) +def build_parser_create_tag(subparser): + parser = subparser.add_parser("create-tag", help="Create a new tag") + + parser.set_defaults(func=run_create_tag) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_tag(parser) + add_arg_image(parser) + + def build_parser_list_builds(subparser): parser = subparser.add_parser("list-builds", help="List repository builds") @@ -492,6 +529,7 @@ def build_parser(): build_parser_list_tags(subparser) build_parser_show_tag(subparser) + build_parser_create_tag(subparser) build_parser_list_builds(subparser) build_parser_show_build(subparser) -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:28PM +0200, Andrea Bolognani wrote:
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/guests/quayadmin b/guests/quayadmin index a35996e..36ce183 100755 --- a/guests/quayadmin +++ b/guests/quayadmin @@ -259,6 +259,25 @@ def run_create_tag(config, args): args.repo)) +def run_delete_tag(config, args): + endpoint = "/repository/{}/{}/tag/{}".format(args.namespace, args.repo, + args.tag) + + res = delete(config, endpoint, debug=args.debug) + + if has_error(args.quiet, res, 204, + "Cannot delete tag {} from repository {}/{}" + .format(args.tag, args.namespace, args.repo)): + return 1 + + if args.quiet: + return 0 + + print("Tag {} deleted from repository {}/{}".format(args.tag, + args.namespace, + args.repo)) + + def run_list_builds(config, args): endpoint = "/repository/{}/{}/build".format(args.namespace, args.repo) @@ -462,6 +481,16 @@ def build_parser_create_tag(subparser): add_arg_image(parser) +def build_parser_delete_tag(subparser): + parser = subparser.add_parser("delete-tag", help="Delete an existing tag") + + parser.set_defaults(func=run_delete_tag) + + add_arg_namespace(parser) + add_arg_repo(parser) + add_arg_tag(parser) + + def build_parser_list_builds(subparser): parser = subparser.add_parser("list-builds", help="List repository builds") @@ -530,6 +559,7 @@ def build_parser(): build_parser_list_tags(subparser) build_parser_show_tag(subparser) build_parser_create_tag(subparser) + build_parser_delete_tag(subparser) build_parser_list_builds(subparser) build_parser_show_build(subparser) -- 2.21.0

On Wed, Jul 17, 2019 at 03:49:29PM +0200, Andrea Bolognani wrote:
Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- guests/quayadmin | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|
participants (2)
-
Andrea Bolognani
-
Daniel P. Berrangé