diff --git a/contrib/registryapi/README.md b/contrib/registryapi/README.md new file mode 100644 index 000000000..5a2d71bf4 --- /dev/null +++ b/contrib/registryapi/README.md @@ -0,0 +1,29 @@ +# registryapi +api for docker registry by token authorization + ++ a simple api class which lies in registryapi.py, which simulates the interactions +between docker registry and the vendor authorization platform like harbor. +``` +usage: +from registryapi import RegistryApi +api = RegistryApi('username', 'password', 'http://www.your_registry_url.com/') +repos = api.getRepositoryList() +tags = api.getTagList('public/ubuntu') +manifest = api.getManifest('public/ubuntu', 'latest') +res = api.deleteManifest('public/ubuntu', '23424545**4343') + +``` + ++ a simple client tool based on api class, which contains basic read and delete +operations for repo, tag, manifest +``` +usage: +./cli.py --username username --password passwrod --registry_endpoint http://www.your_registry_url.com/ target action params + +target can be: repo, tag, manifest +action can be: list, get, delete +params can be: --repo --ref --tag + +more see: ./cli.py -h + +``` diff --git a/contrib/registryapi/cli.py b/contrib/registryapi/cli.py new file mode 100755 index 000000000..298c0523f --- /dev/null +++ b/contrib/registryapi/cli.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +# bug-report: feilengcui008@gmail.com + +""" cli tool """ + +import argparse +import sys +import json +from registry import RegistryApi + + +class ApiProxy(object): + """ user RegistryApi """ + def __init__(self, registry, args): + self.registry = registry + self.args = args + self.callbacks = dict() + self.register_callback("repo", "list", self.list_repo) + self.register_callback("tag", "list", self.list_tag) + self.register_callback("tag", "delete", self.delete_tag) + self.register_callback("manifest", "list", self.list_manifest) + self.register_callback("manifest", "delete", self.delete_manifest) + self.register_callback("manifest", "get", self.get_manifest) + + def register_callback(self, target, action, func): + """ register real actions """ + if not target in self.callbacks.keys(): + self.callbacks[target] = {action: func} + return + self.callbacks[target][action] = func + + def execute(self, target, action): + """ execute """ + print json.dumps(self.callbacks[target][action](), indent=4, sort_keys=True) + + def list_repo(self): + """ list repo """ + return self.registry.getRepositoryList(self.args.num) + + def list_tag(self): + """ list tag """ + return self.registry.getTagList(self.args.repo) + + def delete_tag(self): + """ delete tag """ + (_, ref) = self.registry.existManifest(self.args.repo, self.args.tag) + if ref is not None: + return self.registry.deleteManifest(self.args.repo, ref) + return False + + def list_manifest(self): + """ list manifest """ + tags = self.registry.getTagList(self.args.repo)["tags"] + manifests = list() + if tags is None: + return None + for i in tags: + content = self.registry.getManifestWithConf(self.args.repo, i) + manifests.append({i: content}) + return manifests + + def delete_manifest(self): + """ delete manifest """ + return self.registry.deleteManifest(self.args.repo, self.args.ref) + + def get_manifest(self): + """ get manifest """ + return self.registry.getManifestWithConf(self.args.repo, self.args.tag) + + +# since just a script tool, we do not construct whole target->action->args +# structure with oo abstractions which has more flexibility, just register +# parser directly +def get_parser(): + """ return a parser """ + parser = argparse.ArgumentParser("cli") + + parser.add_argument('--username', action='store', required=True, help='username') + parser.add_argument('--password', action='store', required=True, help='password') + parser.add_argument('--registry_endpoint', action='store', required=True, + help='registry endpoint') + + subparsers = parser.add_subparsers(dest='target', help='target to operate on') + + # repo target + repo_target_parser = subparsers.add_parser('repo', help='target repository') + repo_target_subparsers = repo_target_parser.add_subparsers(dest='action', + help='repository subcommand') + repo_cmd_parser = repo_target_subparsers.add_parser('list', help='list repositories') + repo_cmd_parser.add_argument('--num', action='store', required=False, default=None, + help='the number of data to return') + + # tag target + tag_target_parser = subparsers.add_parser('tag', help='target tag') + tag_target_subparsers = tag_target_parser.add_subparsers(dest='action', + help='tag subcommand') + tag_list_parser = tag_target_subparsers.add_parser('list', help='list tags') + tag_list_parser.add_argument('--repo', action='store', required=True, help='list tags') + tag_delete_parser = tag_target_subparsers.add_parser('delete', help='delete tag') + tag_delete_parser.add_argument('--repo', action='store', required=True, help='delete tags') + tag_delete_parser.add_argument('--tag', action='store', required=True, + help='tag reference') + + # manifest target + manifest_target_parser = subparsers.add_parser('manifest', help='target manifest') + manifest_target_subparsers = manifest_target_parser.add_subparsers(dest='action', + help='manifest subcommand') + manifest_list_parser = manifest_target_subparsers.add_parser('list', help='list manifests') + manifest_list_parser.add_argument('--repo', action='store', required=True, + help='list manifests') + manifest_delete_parser = manifest_target_subparsers.add_parser('delete', help='delete manifest') + manifest_delete_parser.add_argument('--repo', action='store', required=True, + help='delete manifest') + manifest_delete_parser.add_argument('--ref', action='store', required=True, + help='manifest reference') + manifest_get_parser = manifest_target_subparsers.add_parser('get', help='get manifest content') + manifest_get_parser.add_argument('--repo', action='store', required=True, help='delete tags') + manifest_get_parser.add_argument('--tag', action='store', required=True, + help='manifest reference') + + return parser + + +def main(): + """ main entrance """ + parser = get_parser() + options = parser.parse_args(sys.argv[1:]) + registry = RegistryApi(options.username, options.password, options.registry_endpoint) + proxy = ApiProxy(registry, options) + proxy.execute(options.target, options.action) + + +if __name__ == '__main__': + main() diff --git a/contrib/registryapi/registry.py b/contrib/registryapi/registry.py new file mode 100644 index 000000000..66cc74120 --- /dev/null +++ b/contrib/registryapi/registry.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +# bug-report: feilengcui008@gmail.com + +""" api for docker registry """ + +import urllib2 +import urllib +import json +import base64 + + +class RegistryException(Exception): + """ registry api related exception """ + pass + + +class RegistryApi(object): + """ interact with docker registry and harbor """ + def __init__(self, username, password, registry_endpoint): + self.username = username + self.password = password + self.basic_token = base64.encodestring("%s:%s" % (str(username), str(password))) + self.registry_endpoint = registry_endpoint.rstrip('/') + auth = self.pingRegistry("%s/v2/_catalog" % (self.registry_endpoint,)) + if auth is None: + raise RegistryException("get token realm and service failed") + self.token_endpoint = auth[0] + self.service = auth[1] + + def pingRegistry(self, registry_endpoint): + """ ping v2 registry and get realm and service """ + headers = dict() + try: + res = urllib2.urlopen(registry_endpoint) + except urllib2.HTTPError as e: + headers = e.hdrs.dict + try: + (realm, service, _) = headers['www-authenticate'].split(',') + return (realm[14:-1:], service[9:-1]) + except Exception as e: + return None + + def getBearerTokenForScope(self, scope): + """ get bearer token from harbor """ + payload = urllib.urlencode({'service': self.service, 'scope': scope}) + url = "%s?%s" % (self.token_endpoint, payload) + req = urllib2.Request(url) + req.add_header('Authorization', 'Basic %s' % (self.basic_token,)) + try: + response = urllib2.urlopen(req) + return json.loads(response.read())["token"] + except Exception as e: + return None + + def getRepositoryList(self, n=None): + """ get repository list """ + scope = "registry:catalog:*" + bear_token = self.getBearerTokenForScope(scope) + if bear_token is None: + return None + url = "%s/v2/_catalog" % (self.registry_endpoint,) + if n is not None: + url = "%s?n=%s" % (url, str(n)) + req = urllib2.Request(url) + req.add_header('Authorization', r'Bearer %s' % (bear_token,)) + try: + response = urllib2.urlopen(req) + return json.loads(response.read()) + except Exception as e: + return None + + def getTagList(self, repository): + """ get tag list for repository """ + scope = "repository:%s:pull" % (repository,) + bear_token = self.getBearerTokenForScope(scope) + if bear_token is None: + return None + url = "%s/v2/%s/tags/list" % (self.registry_endpoint, repository) + req = urllib2.Request(url) + req.add_header('Authorization', r'Bearer %s' % (bear_token,)) + try: + response = urllib2.urlopen(req) + return json.loads(response.read()) + except Exception as e: + return None + + def getManifest(self, repository, reference="latest", v1=False): + """ get manifest for tag or digest """ + scope = "repository:%s:pull" % (repository,) + bear_token = self.getBearerTokenForScope(scope) + if bear_token is None: + return None + url = "%s/v2/%s/manifests/%s" % (self.registry_endpoint, repository, reference) + req = urllib2.Request(url) + req.get_method = lambda: 'GET' + req.add_header('Authorization', r'Bearer %s' % (bear_token,)) + req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v2+json') + if v1: + req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v1+json') + try: + response = urllib2.urlopen(req) + return json.loads(response.read()) + except Exception as e: + return None + + def existManifest(self, repository, reference, v1=False): + """ check to see it manifest exist """ + scope = "repository:%s:pull" % (repository,) + bear_token = self.getBearerTokenForScope(scope) + if bear_token is None: + raise RegistryException("manifestExist failed due to token error") + url = "%s/v2/%s/manifests/%s" % (self.registry_endpoint, repository, reference) + req = urllib2.Request(url) + req.get_method = lambda: 'HEAD' + req.add_header('Authorization', r'Bearer %s' % (bear_token,)) + req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v2+json') + if v1: + req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v1+json') + try: + response = urllib2.urlopen(req) + return (True, response.headers.dict["docker-content-digest"]) + except Exception as e: + return (False, None) + + def deleteManifest(self, repository, reference): + """ delete manifest by tag """ + (is_exist, digest) = self.existManifest(repository, reference) + if not is_exist: + raise RegistryException("manifest not exist") + scope = "repository:%s:pull,push" % (repository,) + bear_token = self.getBearerTokenForScope(scope) + if bear_token is None: + raise RegistryException("delete manifest failed due to token error") + url = "%s/v2/%s/manifests/%s" % (self.registry_endpoint, repository, digest) + req = urllib2.Request(url) + req.get_method = lambda: 'DELETE' + req.add_header('Authorization', r'Bearer %s' % (bear_token,)) + try: + urllib2.urlopen(req) + except Exception as e: + return False + return True + + def getManifestWithConf(self, repository, reference="latest", v1=False): + """ get manifest for tag or digest """ + manifest = self.getManifest(repository, reference, v1) + if manifest is None: + raise RegistryException("manifest for %s %s not exist" % (repository, reference)) + config_digest = manifest["config"]["digest"] + scope = "repository:%s:pull" % (repository,) + bear_token = self.getBearerTokenForScope(scope) + if bear_token is None: + return None + url = "%s/v2/%s/blobs/%s" % (self.registry_endpoint, repository, config_digest) + req = urllib2.Request(url) + req.get_method = lambda: 'GET' + req.add_header('Authorization', r'Bearer %s' % (bear_token,)) + req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v2+json') + if v1: + req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v1+json') + try: + response = urllib2.urlopen(req) + manifest["configContent"] = json.loads(response.read()) + return manifest + except Exception as e: + return None