Repository: zalando-stups/pierone-cli

Issue:
Pierone login accepts any URL
Would be nice to validate if the pierone URL is actually valid. Maybe pinging the address to see if it works and showing an error. The current behaviour leads to user that committed a typo in the pierone URL to think they are logged-in and getting error from `docker push` later with a not very helpful message.

### Current behaviour
Example of what currently happens:
```
$ pierone login --url registry.does-not-exist.example.com
Getting OAuth2 token "pierone".. OK
Storing Docker client configuration in /home/master/.docker/config.json.. OK
```

Then trying to push image using docker cli:
```
$ docker push pierone.opensource.zalan.do/bus/hello:b17
The push refers to a repository [pierone.opensource.zalan.do/bus/hello]
9c445b8a75e0: Preparing
8a48ff634f1d: Preparing
...
19429b698a22: Waiting
9436069b92a3: Waiting
no basic auth credentials
```
This leads users to think there is a problem with Pierone registry or with Docker which is misleading.

### Suggested behaviour
When trying to login in pierone with a URL of non-pierone server:
```
$ pierone login --url registry.does-not-exist.example.com
ERROR: Not found a valid Pierone registry at registry.does-not-exist.example.com
```


Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index e417a33..7c746c5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,7 @@ python:
 install:
   - pip install -r requirements.txt
   - pip install coveralls
+  - pip install flake8 # forcing installation of flake8, might be removed after https://gitlab.com/pycqa/flake8/issues/164 gets fixed.
 script:
   - python setup.py test
   - python setup.py flake8
diff --git a/pierone/cli.py b/pierone/cli.py
index 90bb5c2..8918a42 100644
--- a/pierone/cli.py
+++ b/pierone/cli.py
@@ -8,7 +8,9 @@ import pierone
 import requests
 import stups_cli.config
 import zign.api
-from clickclick import AliasedGroup, OutputFormat, UrlType, error, print_table
+from clickclick import (AliasedGroup, OutputFormat, UrlType, error,
+                        fatal_error, print_table)
+from requests import RequestException
 
 from .api import (DockerImage, Unauthorized, docker_login, get_image_tags,
                   get_latest_tag, parse_time, request)
@@ -76,6 +78,17 @@ def print_version(ctx, param, value):
     ctx.exit()
 
 
+def validate_pierone_url(url: str) -> None:
+    ping_url = url.rstrip('/') + '/swagger.json'
+    try:
+        response = requests.get(ping_url, timeout=5)
+        response.raise_for_status()
+        if 'Pier One API' not in response.text:
+            fatal_error('ERROR: Did not find a valid Pier One registry at {}'.format(url))
+    except RequestException:
+        fatal_error('ERROR: Could not reach {}'.format(ping_url))
+
+
 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')
@@ -93,6 +106,7 @@ def set_pierone_url(config: dict, url: str) -> None:
         # issue 63: gracefully handle URLs without scheme
         url = 'https://{}'.format(url)
 
+    validate_pierone_url(url)
     config['url'] = url
     return url
 


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index e76073c..0bdd2fe 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -3,8 +3,17 @@ import os
 import re
 from unittest.mock import MagicMock
 
+import pytest
 from click.testing import CliRunner
 from pierone.cli import cli
+from requests import RequestException
+
+
+@pytest.fixture(autouse=True)
+def valid_pierone_url(monkeypatch):
+    response = MagicMock()
+    response.text = 'Pier One API'
+    monkeypatch.setattr('requests.get', lambda *args, **kw: response)
 
 
 def test_version(monkeypatch):
@@ -16,22 +25,47 @@ def test_version(monkeypatch):
 
 
 def test_login(monkeypatch, tmpdir):
-    response = MagicMock()
-
     runner = CliRunner()
 
     monkeypatch.setattr('stups_cli.config.load_config', lambda x: {})
     monkeypatch.setattr('pierone.api.get_named_token', MagicMock(return_value={'access_token': 'tok123'}))
     monkeypatch.setattr('os.path.expanduser', lambda x: x.replace('~', str(tmpdir)))
-    monkeypatch.setattr('requests.get', lambda x, timeout: response)
 
     with runner.isolated_filesystem():
         result = runner.invoke(cli, ['login'], catch_exceptions=False, input='pieroneurl\n')
+        assert 'Storing Docker client configuration' in result.output
+        assert result.output.rstrip().endswith('OK')
         with open(os.path.join(str(tmpdir), '.docker/config.json')) as fd:
             data = json.load(fd)
         assert data['auths']['https://pieroneurl']['auth'] == 'b2F1dGgyOnRvazEyMw=='
-        assert 'Storing Docker client configuration' in result.output
-        assert result.output.rstrip().endswith('OK')
+
+
+def test_invalid_url_for_login(monkeypatch, tmpdir):
+    runner = CliRunner()
+    response = MagicMock()
+
+    monkeypatch.setattr('stups_cli.config.load_config', lambda x: {})
+    monkeypatch.setattr('pierone.api.get_named_token', MagicMock(return_value={'access_token': 'tok123'}))
+    monkeypatch.setattr('os.path.expanduser', lambda x: x.replace('~', str(tmpdir)))
+
+    # Missing Pier One header
+    response.text = 'Not valid API'
+    monkeypatch.setattr('requests.get', lambda *args, **kw: response)
+
+    with runner.isolated_filesystem():
+        result = runner.invoke(cli, ['login'], catch_exceptions=False, input='pieroneurl\n')
+        assert 'ERROR: Did not find a valid Pier One registry at https://pieroneurl' in result.output
+        assert result.exit_code == 1
+        assert not os.path.exists(os.path.join(str(tmpdir), '.docker/config.json'))
+
+    # Not a valid header
+    response.raise_for_status = MagicMock(side_effect=RequestException)
+    monkeypatch.setattr('requests.get', lambda *args, **kw: response)
+    with runner.isolated_filesystem():
+        result = runner.invoke(cli, ['login'], catch_exceptions=False, input='pieroneurl\n')
+        assert 'ERROR: Could not reach https://pieroneurl' in result.output
+        assert result.exit_code == 1
+        assert not os.path.exists(os.path.join(str(tmpdir), '.docker/config.json'))
 
 
 def test_login_arg_user(monkeypatch, tmpdir):
@@ -95,8 +129,6 @@ def test_login_env_user(monkeypatch, tmpdir):
 
 
 def test_login_given_url_option(monkeypatch, tmpdir):
-    response = MagicMock()
-
     runner = CliRunner()
 
     config = {}
@@ -108,7 +140,6 @@ def test_login_given_url_option(monkeypatch, tmpdir):
     monkeypatch.setattr('stups_cli.config.store_config', store)
     monkeypatch.setattr('pierone.api.get_named_token', MagicMock(return_value={'access_token': 'tok123'}))
     monkeypatch.setattr('os.path.expanduser', lambda x: x.replace('~', str(tmpdir)))
-    monkeypatch.setattr('requests.get', lambda x, timeout: response)
 
     with runner.isolated_filesystem():
         runner.invoke(cli, ['login'], catch_exceptions=False, input='pieroneurl\n')