PK֚wGGۧTDDpierone/__main__.pyfrom pierone.cli import main if __name__ == '__main__': main() PK֚wG>)wpierone/__init__.py__version__ = '1.0.7' PK֚wGdXpierone/api.pyimport codecs import json import os from clickclick import Action import collections import requests from zign.api import get_named_token, get_existing_token adapter = requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10) session = requests.Session() session.mount('http://', adapter) session.mount('https://', adapter) class Unauthorized(Exception): def __str__(self): return 'Unauthorized: token missing or invalid' class DockerImage(collections.namedtuple('DockerImage', 'registry team artifact tag')): @classmethod def parse(cls, image: str): ''' >>> DockerImage.parse('x') Traceback (most recent call last): ValueError: Invalid docker image "x" (format must be [REGISTRY/]TEAM/ARTIFACT:TAG) >>> DockerImage.parse('foo/bar') DockerImage(registry=None, team='foo', artifact='bar', tag='') >>> DockerImage.parse('registry/foo/bar:1.9') DockerImage(registry='registry', team='foo', artifact='bar', tag='1.9') ''' parts = image.split('/') if len(parts) == 3: registry = parts[0] elif len(parts) < 2: raise ValueError('Invalid docker image "{}" (format must be [REGISTRY/]TEAM/ARTIFACT:TAG)'.format(image)) else: registry = None team = parts[-2] artifact, sep, tag = parts[-1].partition(':') return DockerImage(registry=registry, team=team, artifact=artifact, tag=tag) def __str__(self): ''' >>> str(DockerImage(registry='registry', team='foo', artifact='bar', tag='1.9')) 'registry/foo/bar:1.9' ''' return '{}/{}/{}:{}'.format(*tuple(self)) def docker_login(url, realm, name, user, password, token_url=None, use_keyring=True, prompt=False): with Action('Getting OAuth2 token "{}"..'.format(name)): token = get_named_token(['uid'], realm, name, user, password, url=token_url, use_keyring=use_keyring, prompt=prompt) access_token = token.get('access_token') docker_login_with_token(url, access_token) def docker_login_with_token(url, access_token): '''Configure docker with existing OAuth2 access token''' path = os.path.expanduser('~/.docker/config.json') try: with open(path) as fd: dockercfg = json.load(fd) except: dockercfg = {} basic_auth = codecs.encode('oauth2:{}'.format(access_token).encode('utf-8'), 'base64').strip().decode('utf-8') if 'auths' not in dockercfg: dockercfg['auths'] = {} dockercfg['auths'][url] = {'auth': basic_auth, 'email': 'no-mail-required@example.org'} with Action('Storing Docker client configuration in {}..'.format(path)): os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'w') as fd: json.dump(dockercfg, fd) def request(url, path, access_token): return session.get('{}{}'.format(url, path), headers={'Authorization': 'Bearer {}'.format(access_token)}, timeout=10) def image_exists(token_name: str, image: DockerImage) -> bool: token = get_existing_token(token_name) if not token: raise Unauthorized() url = 'https://{}'.format(image.registry) path = '/v1/repositories/{team}/{artifact}/tags'.format(team=image.team, artifact=image.artifact) try: r = request(url, path, token['access_token']) except: return False result = r.json() return image.tag in result def get_latest_tag(token_name: str, image: DockerImage) -> bool: token = get_existing_token(token_name) if not token: raise Unauthorized() url = 'https://{}'.format(image.registry) path = '/teams/{team}/artifacts/{artifact}/tags'.format(team=image.team, artifact=image.artifact) try: r = request(url, path, token['access_token']) except: return None result = r.json() if result: return sorted(result, key=lambda x: x['created'])[-1]['name'] else: return None PK֚wG14L  pierone/cli.pyimport datetime import os import re import click import requests import time import zign.api from clickclick import error, AliasedGroup, print_table, OutputFormat from .api import docker_login, request, get_latest_tag, DockerImage import pierone import stups_cli.config KEYRING_KEY = 'pierone' CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) output_option = click.option('-o', '--output', type=click.Choice(['text', 'json', 'tsv']), default='text', help='Use alternative output format') url_option = click.option('--url', help='Pier One URL', metavar='URI') TEAM_PATTERN_STR = r'[a-z][a-z0-9-]+' TEAM_PATTERN = re.compile(r'^{}$'.format(TEAM_PATTERN_STR)) def validate_team(ctx, param, value): if not TEAM_PATTERN.match(value): msg = 'Team ID must satisfy regular expression pattern "{}"'.format(TEAM_PATTERN_STR) raise click.BadParameter(msg) return value def parse_time(s: str) -> float: ''' >>> parse_time('2015-04-14T19:09:01.000Z') > 0 True ''' try: utc = datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%fZ') ts = time.time() utc_offset = datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp(ts) local = utc + utc_offset return local.timestamp() except Exception as e: print(e) return None def print_version(ctx, param, value): if not value or ctx.resilient_parsing: return click.echo('Pier One CLI {}'.format(pierone.__version__)) ctx.exit() def set_pierone_url(config: dict, url: str) -> None: '''Read Pier One URL from cli, from config file or from stdin.''' url = url or config.get('url') while not url: url = click.prompt('Please enter the Pier One URL') if not url.startswith('http'): url = 'https://{}'.format(url) try: requests.get(url, timeout=5) except: error('Could not reach {}'.format(url)) url = None config['url'] = url @click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS) @click.option('-V', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True, help='Print the current version number and exit.') @click.pass_context def cli(ctx): ctx.obj = stups_cli.config.load_config('pierone') @cli.command() @url_option @click.option('--realm', help='Use custom OAuth2 realm', metavar='NAME') @click.option('-n', '--name', help='Custom token name (will be stored)', metavar='TOKEN_NAME', default='pierone') @click.option('-U', '--user', help='Username to use for authentication', envvar='PIERONE_USER', metavar='NAME') @click.option('-p', '--password', help='Password to use for authentication', envvar='PIERONE_PASSWORD', metavar='PWD') @click.pass_obj def login(config, url, realm, name, user, password): '''Login to Pier One Docker registry (generates ~/.dockercfg''' set_pierone_url(config, url) user = user or os.getenv('USER') stups_cli.config.store_config(config, 'pierone') docker_login(url, realm, name, user, password, prompt=True) def get_token(): try: token = zign.api.get_token('pierone', ['uid']) except Exception as e: raise click.UsageError(str(e)) return token @cli.command() @url_option @output_option @click.pass_obj def teams(config, output, url): '''List all teams having artifacts in Pier One''' set_pierone_url(config, url) token = get_token() r = request(config.get('url'), '/teams', token) rows = [{'name': name} for name in sorted(r.json())] with OutputFormat(output): print_table(['name'], rows) def get_artifacts(url, team, access_token): r = request(url, '/teams/{}/artifacts'.format(team), access_token) return r.json() def get_tags(url, team, art, access_token): r = request(url, '/teams/{}/artifacts/{}/tags'.format(team, art), access_token) return r.json() @cli.command() @click.argument('team', callback=validate_team) @url_option @output_option @click.pass_obj def artifacts(config, team, url, output): '''List all team artifacts''' set_pierone_url(config, url) token = get_token() result = get_artifacts(config.get('url'), team, token) rows = [{'team': team, 'artifact': name} for name in sorted(result)] with OutputFormat(output): print_table(['team', 'artifact'], rows) @cli.command() @click.argument('team', callback=validate_team) @click.argument('artifact', nargs=-1) @url_option @output_option @click.pass_obj def tags(config, team, artifact, url, output): '''List all tags for a given team''' set_pierone_url(config, url) token = get_token() if not artifact: artifact = get_artifacts(config.get('url'), team, token) rows = [] for art in artifact: r = get_tags(config.get('url'), team, art, token) rows.extend([{'team': team, 'artifact': art, 'tag': row['name'], 'created_by': row['created_by'], 'created_time': parse_time(row['created'])} for row in r]) rows.sort(key=lambda row: (row['team'], row['artifact'], row['tag'])) with OutputFormat(output): print_table(['team', 'artifact', 'tag', 'created_time', 'created_by'], rows, titles={'created_time': 'Created', 'created_by': 'By'}) @cli.command() @click.argument('team', callback=validate_team) @click.argument('artifact') @url_option @output_option @click.pass_obj def latest(config, team, artifact, url, output): '''Get latest tag/version of a specific artifact''' # validate that the token exists! set_pierone_url(config, url) get_token() registry = config.get('url') if registry.startswith('https://'): registry = registry[8:] image = DockerImage(registry=registry, team=team, artifact=artifact, tag=None) print(get_latest_tag('pierone', image)) @cli.command('scm-source') @click.argument('team', callback=validate_team) @click.argument('artifact') @click.argument('tag', nargs=-1) @url_option @output_option @click.pass_obj def scm_source(config, team, artifact, tag, url, output): '''Show SCM source information such as GIT revision''' set_pierone_url(config, url) token = get_token() tags = get_tags(config.get('url'), team, artifact, token) if not tag: tag = [t['name'] for t in tags] rows = [] for t in tag: row = request(config.get('url'), '/teams/{}/artifacts/{}/tags/{}/scm-source'.format(team, artifact, t), token).json() if not row: row = {} row['tag'] = t matching_tag = [d for d in tags if d['name'] == t] row['created_by'] = ''.join([d['created_by'] for d in matching_tag]) if matching_tag: row['created_time'] = parse_time(''.join([d['created'] for d in matching_tag])) rows.append(row) rows.sort(key=lambda row: (row['tag'], row.get('created_time'))) with OutputFormat(output): print_table(['tag', 'author', 'url', 'revision', 'status', 'created_time', 'created_by'], rows, titles={'tag': 'Tag', 'created_by': 'By', 'created_time': 'Created', 'url': 'URL', 'revision': 'Revision', 'status': 'Status'}, max_column_widths={'revision': 10}) @cli.command('image') @click.argument('image') @url_option @output_option @click.pass_obj def image(config, image, url, output): '''List tags that point to this image''' set_pierone_url(config, url) token = get_token() resp = request(config.get('url'), '/tags/{}'.format(image), token) if resp.status_code == 404: click.echo('Image {} not found'.format(image)) return if resp.status_code == 412: click.echo('Prefix {} matches more than one image.'.format(image)) return tags = resp.json() with OutputFormat(output): print_table(['team', 'artifact', 'name'], tags, titles={'name': 'Tag', 'artifact': 'Artifact', 'team': 'Team'}) def main(): cli() PK֚wGp%8-stups_pierone-1.0.7.dist-info/DESCRIPTION.rst============ Pier One CLI ============ .. image:: https://travis-ci.org/zalando-stups/pierone-cli.svg?branch=master :target: https://travis-ci.org/zalando-stups/pierone-cli :alt: Build Status .. image:: https://coveralls.io/repos/zalando-stups/pierone-cli/badge.svg :target: https://coveralls.io/r/zalando-stups/pierone-cli :alt: Code Coverage .. image:: https://img.shields.io/pypi/dw/stups-pierone.svg :target: https://pypi.python.org/pypi/stups-pierone/ :alt: PyPI Downloads .. image:: https://img.shields.io/pypi/v/stups-pierone.svg :target: https://pypi.python.org/pypi/stups-pierone/ :alt: Latest PyPI version .. image:: https://img.shields.io/pypi/l/stups-pierone.svg :target: https://pypi.python.org/pypi/stups-pierone/ :alt: License Convenience command line tool for Pier One Docker registry. .. code-block:: bash $ sudo pip3 install --upgrade stups-pierone Usage ===== .. code-block:: bash $ pierone login $ pierone teams See the `STUPS documentation on pierone`_ for details. You can also run it locally from source: .. code-block:: bash $ python3 -m pierone Running Unit Tests ================== .. code-block:: bash $ python3 setup.py test --cov-html=true .. _STUPS documentation on pierone: http://stups.readthedocs.org/en/latest/components/pierone.html Releasing ========= .. code-block:: bash $ ./release.sh PK֚wGj...stups_pierone-1.0.7.dist-info/entry_points.txt[console_scripts] pierone = pierone.cli:main PK֚wG!+stups_pierone-1.0.7.dist-info/metadata.json{"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython"], "extensions": {"python.commands": {"wrap_console": {"pierone": "pierone.cli:main"}}, "python.details": {"contacts": [{"email": "henning.jacobs@zalando.de", "name": "Henning Jacobs", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/zalando-stups/pierone-cli"}}, "python.exports": {"console_scripts": {"pierone": "pierone.cli:main"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["pier", "one", "docker", "registry"], "license": "Apache License 2.0", "metadata_version": "2.0", "name": "stups-pierone", "run_requires": [{"requires": ["clickclick (>=0.9)", "keyring", "requests", "stups-cli-support", "stups-zign (>=1.0.13)"]}], "summary": "Pier One Docker registry CLI", "test_requires": [{"requires": ["pytest", "pytest-cov"]}], "version": "1.0.7"}PK֚wG $+stups_pierone-1.0.7.dist-info/top_level.txtpierone PK֚wG}\\#stups_pierone-1.0.7.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py3-none-any PK֚wGR6 &stups_pierone-1.0.7.dist-info/METADATAMetadata-Version: 2.0 Name: stups-pierone Version: 1.0.7 Summary: Pier One Docker registry CLI Home-page: https://github.com/zalando-stups/pierone-cli Author: Henning Jacobs Author-email: henning.jacobs@zalando.de License: Apache License 2.0 Keywords: pier one docker registry Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Dist: clickclick (>=0.9) Requires-Dist: keyring Requires-Dist: requests Requires-Dist: stups-cli-support Requires-Dist: stups-zign (>=1.0.13) ============ Pier One CLI ============ .. image:: https://travis-ci.org/zalando-stups/pierone-cli.svg?branch=master :target: https://travis-ci.org/zalando-stups/pierone-cli :alt: Build Status .. image:: https://coveralls.io/repos/zalando-stups/pierone-cli/badge.svg :target: https://coveralls.io/r/zalando-stups/pierone-cli :alt: Code Coverage .. image:: https://img.shields.io/pypi/dw/stups-pierone.svg :target: https://pypi.python.org/pypi/stups-pierone/ :alt: PyPI Downloads .. image:: https://img.shields.io/pypi/v/stups-pierone.svg :target: https://pypi.python.org/pypi/stups-pierone/ :alt: Latest PyPI version .. image:: https://img.shields.io/pypi/l/stups-pierone.svg :target: https://pypi.python.org/pypi/stups-pierone/ :alt: License Convenience command line tool for Pier One Docker registry. .. code-block:: bash $ sudo pip3 install --upgrade stups-pierone Usage ===== .. code-block:: bash $ pierone login $ pierone teams See the `STUPS documentation on pierone`_ for details. You can also run it locally from source: .. code-block:: bash $ python3 -m pierone Running Unit Tests ================== .. code-block:: bash $ python3 setup.py test --cov-html=true .. _STUPS documentation on pierone: http://stups.readthedocs.org/en/latest/components/pierone.html Releasing ========= .. code-block:: bash $ ./release.sh PK֚wGg $stups_pierone-1.0.7.dist-info/RECORDpierone/__init__.py,sha256=Th6FPUfmw3s94879QiaD_OgzmQyEFPOnYrMVoQqee8Y,22 pierone/__main__.py,sha256=vLe25QblsBlL3TEEcX-FI86zFKupVHiQvdHsBncJeho,68 pierone/api.py,sha256=gRol_D96HUmLgkTJDvSWCYeINX51bcHixVC13GsQWAY,4071 pierone/cli.py,sha256=zfDRzVJRYcMA4bmdGyhAZm_o1GvM-YBFCPN-Bb8Lz2A,8214 stups_pierone-1.0.7.dist-info/DESCRIPTION.rst,sha256=XCUXnYHpz0bF6_awG-n_vgATXlC1Dw9fP2-tmhCIa98,1416 stups_pierone-1.0.7.dist-info/METADATA,sha256=Q0ssIWjZ7c9Ls-WJtnHJzXtZl9Sf4Cf_cYxYYIYbp-Q,2315 stups_pierone-1.0.7.dist-info/RECORD,, stups_pierone-1.0.7.dist-info/WHEEL,sha256=zX7PHtH_7K-lEzyK75et0UBa3Bj8egCBMXe1M4gc6SU,92 stups_pierone-1.0.7.dist-info/entry_points.txt,sha256=49xnYiTiPYsncOBz-xtvx86vfxAQVherWjNv6LPEogg,46 stups_pierone-1.0.7.dist-info/metadata.json,sha256=iG54_f_tC1x2cRUdT9m0PRsRUm4vnUBrftHI9YQbB60,1226 stups_pierone-1.0.7.dist-info/top_level.txt,sha256=QrjhRey0_V3LLS_6hNDAztX1xufTw3roqb81Fgu8A-Q,8 PK֚wGGۧTDDpierone/__main__.pyPK֚wG>)wupierone/__init__.pyPK֚wGdXpierone/api.pyPK֚wG14L  pierone/cli.pyPK֚wGp%8-1stups_pierone-1.0.7.dist-info/DESCRIPTION.rstPK֚wGj...6stups_pierone-1.0.7.dist-info/entry_points.txtPK֚wG!+^7stups_pierone-1.0.7.dist-info/metadata.jsonPK֚wG $+q<stups_pierone-1.0.7.dist-info/top_level.txtPK֚wG}\\#<stups_pierone-1.0.7.dist-info/WHEELPK֚wGR6 &_=stups_pierone-1.0.7.dist-info/METADATAPK֚wGg $Fstups_pierone-1.0.7.dist-info/RECORDPK ZJ