summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-04-07 17:09:54 +0300
committerLars Wirzenius <liw@liw.fi>2018-04-07 17:09:54 +0300
commitdf115d5300c8ea64df634c5e06f091667121c8ae (patch)
treee7d5a0b5b614852611a74a8324f17f1eceb40a43
parent5aeef33219103eb5f39bfc0a79ed462f46a12420 (diff)
parent508835b5db20b545d71337d30dfcddc6a71ce737 (diff)
downloadick2-df115d5300c8ea64df634c5e06f091667121c8ae.tar.gz
Merge: get access tokens from authentication server
-rw-r--r--NEWS10
-rw-r--r--ick2/__init__.py1
-rw-r--r--ick2/client.py96
-rw-r--r--ick2/client_tests.py98
-rwxr-xr-xicktool559
-rwxr-xr-xicktool2210
-rw-r--r--pylint.conf1
-rwxr-xr-xrun-debug2
-rwxr-xr-xworker_manager97
9 files changed, 498 insertions, 576 deletions
diff --git a/NEWS b/NEWS
index b5f834e..a9fc49f 100644
--- a/NEWS
+++ b/NEWS
@@ -20,6 +20,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
Version 0.34+git, not yet released
----------------------------------
+* Controller now knows the URL to the identity provider's
+ authentication endpoint. This is reported via `/version`, which now
+ doesn't require authentication.
+
+* Worker-manager and `icktool` now fetch an access token from the IDP,
+ instead of generating token themselves.
+
+* `icktool` has been substantcially rewritten, with much loss of
+ functionality. Lost functionality will be added back as the loss is
+ noticed by users.
Version 0.34, released 2018-04-05
----------------------------------
diff --git a/ick2/__init__.py b/ick2/__init__.py
index 8673046..1f6b514 100644
--- a/ick2/__init__.py
+++ b/ick2/__init__.py
@@ -57,6 +57,7 @@ from .client import (
HttpError,
ControllerClient,
BlobClient,
+ AuthClient,
Reporter,
)
from .actionenvs import (
diff --git a/ick2/client.py b/ick2/client.py
index 8bcf45b..ec2a113 100644
--- a/ick2/client.py
+++ b/ick2/client.py
@@ -13,9 +13,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import base64
import json
import logging
import time
+import urllib
import requests
@@ -64,18 +66,32 @@ class HttpAPI:
self._send_request(self._session.post, url, headers=headers, body=body)
return None
+ def post_auth(self, url, headers=None, body=None, auth=None):
+ assert auth is not None
+ if headers is None:
+ headers = {}
+ headers['Authorization'] = self._basic_auth(auth)
+ return self._send_request(
+ self._session.post, url, headers=headers, body=body, auth=auth)
+
+ def _basic_auth(self, auth):
+ username, password = auth
+ cleartext = '{}:{}'.format(username, password).encode('UTF-8')
+ encoded = base64.b64encode(cleartext)
+ return 'Basic {}'.format(encoded.decode('UTF-8'))
+
def put(self, url, headers=None, body=None):
self._send_request(self._session.put, url, headers=headers, body=body)
return None
- def _send_request(self, func, url, headers=None, body=None):
+ def _send_request(self, func, url, headers=None, body=None, auth=None):
if headers is None:
headers = {}
headers = dict(headers)
- h, body = self._get_content_type_header(body)
- headers.update(h)
- self._request(func, url, headers=headers, data=body)
- return None
+ if not headers.get('Content-Type'):
+ h, body = self._get_content_type_header(body)
+ headers.update(h)
+ return self._request(func, url, headers=headers, data=body, auth=auth)
def _get_content_type_header(self, body):
if isinstance(body, dict):
@@ -94,7 +110,12 @@ class HttpAPI:
def _request(self, func, url, headers=None, **kwargs):
if headers is None:
headers = {}
- headers.update(self._get_authorization_headers())
+
+ auth = kwargs.get('auth')
+ if auth is None:
+ headers.update(self._get_authorization_headers())
+ if 'auth' in kwargs:
+ del kwargs['auth']
r = func(url, headers=headers, verify=self._verify, **kwargs)
if not r.ok:
@@ -108,6 +129,7 @@ class ControllerClient:
self._name = None
self._api = HttpAPI()
self._url = None
+ self._auth_url = None
def set_client_name(self, name):
self._name = name
@@ -127,13 +149,29 @@ class ControllerClient:
def url(self, path):
return '{}{}'.format(self._url, path)
- def get_artifact_store_url(self):
+ def get_version(self):
url = self.url('/version')
- version = self._api.get_dict(url)
+ return self._api.get_dict(url)
+
+ def get_artifact_store_url(self):
+ version = self.get_version()
url = version.get('artifact_store')
logging.info('Artifact store URL: %r', url)
return url
+ def get_auth_url(self):
+ version = self.get_version()
+ url = version.get('auth_url')
+ logging.info('Authentication URL: %r', url)
+ return url
+
+ def get_auth_client(self):
+ url = self.get_auth_url()
+ ac = AuthClient()
+ ac.set_auth_url(url)
+ ac.set_http_api(self._api)
+ return ac
+
def get_blob_client(self):
url = self.get_artifact_store_url()
blobs = BlobClient()
@@ -168,6 +206,48 @@ class ControllerClient:
body = json.dumps(work)
self._api.post(url, headers=headers, body=body)
+ def show(self, path): # pragma: no cover
+ url = self.url(path)
+ return self._api.get_dict(url)
+
+ def create(self, path, obj): # pragma: no cover
+ url = self.url(path)
+ return self._api.post(url, body=obj)
+
+
+class AuthClient:
+
+ def __init__(self):
+ self._auth_url = None
+ self._http_api = HttpAPI()
+ self._client_id = None
+ self._client_secret = None
+
+ def set_auth_url(self, url):
+ self._auth_url = url
+
+ def set_http_api(self, api):
+ self._http_api = api
+
+ def set_client_creds(self, client_id, client_secret):
+ self._client_id = client_id
+ self._client_secret = client_secret
+
+ def get_token(self, scope):
+ auth = (self._client_id, self._client_secret)
+ params = {
+ 'grant_type': 'client_credentials',
+ 'scope': scope,
+ }
+ body = urllib.parse.urlencode(params)
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ }
+ r = self._http_api.post_auth(
+ self._auth_url, headers=headers, body=body, auth=auth)
+ obj = r.json()
+ return obj['access_token']
+
class Reporter: # pragma: no cover
diff --git a/ick2/client_tests.py b/ick2/client_tests.py
index 92da36a..f164e6a 100644
--- a/ick2/client_tests.py
+++ b/ick2/client_tests.py
@@ -97,6 +97,30 @@ class HttpAPITests(unittest.TestCase):
obj = self.client.put('http://controller/work', body=blob)
self.assertEqual(obj, None)
+ def test_post_auth_does_basic_auth(self):
+ token = 'this is a token'
+ scope = 'this and that'
+ response = {
+ 'access_token': token,
+ 'token_type': 'bearer',
+ 'scope': scope,
+ }
+ self.session.response = FakeResponse(200, body=response)
+
+ client_id = 'this-is--my-client'
+ client_secret = '*****'
+ auth = (client_id, client_secret)
+ body = 'foo=bar'
+ self.client.post_auth(
+ 'http://auth.example.com/token', auth=auth, body=body)
+
+ self.assertEqual(self.session.auth, auth)
+
+ authz = self.session.headers['Authorization']
+ self.assertTrue(authz.startswith('Basic '))
+
+ self.assertEqual(self.session.body, body)
+
class ControllerClientTests(unittest.TestCase):
@@ -164,6 +188,70 @@ class ControllerClientTests(unittest.TestCase):
200, body=json.dumps(version), content_type=json_type)
self.assertEqual(self.controller.get_artifact_store_url(), url)
+ def test_get_auth_url_raises_exception_on_error(self):
+ self.session.response = FakeResponse(400)
+ with self.assertRaises(ick2.HttpError):
+ self.controller.get_auth_url()
+
+ def test_get_auth_url_succeeds(self):
+ url = 'https://blobs'
+ version = {
+ 'auth_url': url,
+ }
+ self.session.response = FakeResponse(
+ 200, body=json.dumps(version), content_type=json_type)
+ self.assertEqual(self.controller.get_auth_url(), url)
+
+ def test_get_auth_client_returns_object(self):
+ url = 'https://blobs'
+ version = {
+ 'auth_url': url,
+ }
+ self.session.response = FakeResponse(
+ 200, body=json.dumps(version), content_type=json_type)
+ ac = self.controller.get_auth_client()
+ self.assertTrue(isinstance(ac, ick2.AuthClient))
+
+
+class AuthClientTests(unittest.TestCase):
+
+ def setUp(self):
+ self.session = FakeHttpSession()
+
+ self.client = ick2.HttpAPI()
+ self.client.set_session(self.session)
+
+ def test_raises_exception_on_error(self):
+ self.session.response = FakeResponse(400)
+
+ url = 'https://auth.example.com'
+ client_id = 'test-client'
+ client_secret = 'hunter2'
+ ac = ick2.AuthClient()
+ ac.set_auth_url(url)
+ ac.set_http_api(self.client)
+ ac.set_client_creds(client_id, client_secret)
+ with self.assertRaises(ick2.HttpError):
+ ac.get_token('')
+
+ def test_returns_token(self):
+ token = 'this-is-my-token'
+ token_response = {
+ 'access_token': token,
+ }
+
+ self.session.response = FakeResponse(
+ 200, body=json.dumps(token_response), content_type=json_type)
+
+ url = 'https://auth.example.com'
+ client_id = 'test-client'
+ client_secret = 'hunter2'
+ ac = ick2.AuthClient()
+ ac.set_auth_url(url)
+ ac.set_http_api(self.client)
+ ac.set_client_creds(client_id, client_secret)
+ self.assertEqual(ac.get_token(''), token)
+
class BlobServiceClientTests(unittest.TestCase):
@@ -231,15 +319,21 @@ class FakeHttpSession:
def __init__(self):
self.response = None
self.token = None
+ self.auth = None
+ self.headers = None
+ self.body = None
def get(self, url, headers=None, verify=None):
assert self.response is not None
assert self.is_authorized(headers)
return self.response
- def post(self, url, headers=None, data=None, verify=None):
+ def post(self, url, headers=None, data=None, verify=None, auth=None):
assert self.response is not None
- assert self.is_authorized(headers)
+ assert auth is not None or self.is_authorized(headers)
+ self.auth = auth
+ self.headers = headers
+ self.body = data
return self.response
def put(self, url, headers=None, data=None, verify=None):
diff --git a/icktool b/icktool
index a0ef14e..fa7f8f3 100755
--- a/icktool
+++ b/icktool
@@ -15,15 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import configparser
import json
import logging
import sys
-import time
-import apifw
import cliapp
-import Crypto.PublicKey.RSA
-import requests
import yaml
import ick2
@@ -75,6 +72,18 @@ class Icktool(cliapp.Application):
metavar='URL',
)
+ self.settings.string(
+ ['auth-url'],
+ 'use URL as the authentication URL',
+ metavar='URL',
+ )
+
+ self.settings.string(
+ ['secrets'],
+ 'use URL as the controller base URL',
+ metavar='URL',
+ )
+
self.settings.boolean(
['verify-tls'],
'verify API provider TLS certificate '
@@ -88,15 +97,9 @@ class Icktool(cliapp.Application):
metavar='TOKEN',
)
- self.settings.string(
- ['token-private-key-cmd'],
- 'run CMD to print private key for token signing',
- metavar='CMD',
- )
-
self.settings.string_list(
['scope'],
- 'add SCOPE to the list of scope in generated token',
+ 'add SCOPE to the list of scope in requested token',
metavar='SCOPE',
default=self._default_scopes,
)
@@ -107,507 +110,97 @@ class Icktool(cliapp.Application):
def cmd_scopes(self, args):
for scope in self.settings['scope']:
- sys.stdout.write('{}\n'.format(scope))
+ self.output.write('{}\n'.format(scope))
def cmd_token(self, args):
token = self._new_token()
- sys.stdout.write(token)
+ self.output.write('{}\n'.format(token))
def cmd_version(self, args):
+ token = self._new_token()
api = self._new_api()
+ api.set_token(token)
version = api.get_version()
- if not version:
- sys.exit(1)
self._prettyson(version)
- def cmd_status(self, args):
- rows = []
- projects = self._get_projects()
- project_objs = sorted(
- projects['projects'], key=lambda p: p.get('project'))
- builds = self._get_builds()['builds']
- for project in project_objs:
- pipelines = sorted(project['pipelines'])
- for pipeline in pipelines:
- build = self._get_latest_build(
- project['project'], pipeline, builds)
- if build is None:
- build = {
- 'build_id': 'never',
- 'log': 'none',
- 'status': 'n/a',
- }
- status = self._get_pipeline_status(
- project['project'], pipeline)
- row = {
- 'project': project['project'],
- 'pipeline': pipeline,
- 'build_id': build['build_id'],
- 'status': status['status'],
- 'build_status': build['status'],
- 'log': build['log'],
- }
- rows.append(row)
- self._pretty_table(
- rows, ['project', 'pipeline', 'status', 'build_status', 'log'])
-
- def _get_projects(self):
- rc = self._new_rc('/projects', 'project')
- projects = rc.list()
- return projects
-
- def _get_builds(self):
- rc = self._new_rc('/builds', 'build')
- return rc.list()
-
- def _get_latest_build(self, project, pipeline, builds):
- latest = None
- for build in builds:
- if (build['project'], build['pipeline']) == (project, pipeline):
- latest = build
- return latest
-
- def cmd_make_it_so(self, args):
- obj = self._read_object()
-
- projects = self._new_rc('/projects', 'project')
- self._make_it_so(projects, obj.get('projects', []))
-
- pipelines = self._new_rc('/pipelines', 'pipeline')
- self._make_it_so(pipelines, obj.get('pipelines', []))
-
- def _make_it_so(self, rc, objs):
- for obj in objs:
- if rc.exists(obj):
- rc.update(obj)
- else:
- rc.create(obj)
-
- def cmd_list_projects(self, args):
- self._prettyson(self._get_projects())
-
- def cmd_create_project(self, args):
- rc = self._new_rc('/projects', 'project')
- obj = self._read_object()
- rc.create(obj)
-
- def cmd_update_project(self, args):
- rc = self._new_rc('/projects', 'project')
+ def cmd_make_it_so(self, argv):
obj = self._read_object()
- rc.update(obj)
- def cmd_show_project(self, args):
- rc = self._new_rc('/projects', 'project')
- name = args[0]
- self._prettyson(rc.show(name))
-
- def cmd_delete_project(self, args):
- rc = self._new_rc('/projects', 'project')
- name = args[0]
- rc.delete(name)
-
- def cmd_list_pipelines(self, args):
- rc = self._new_rc('/pipelines', 'pipeline')
- self._prettyson(rc.list())
-
- def cmd_create_pipeline(self, args):
- rc = self._new_rc('/pipelines', 'pipeline')
- obj = self._read_object()
- rc.create(obj)
-
- def cmd_update_pipeline(self, args):
- rc = self._new_rc('/pipelines', 'pipeline')
- obj = self._read_object()
- rc.update(obj)
+ token = self._new_token()
+ api = self._new_api()
+ api.set_token(token)
- def cmd_show_pipeline(self, args):
- rc = self._new_rc('/pipelines', 'pipeline')
- name = args[0]
- self._prettyson(rc.show(name))
+ self._create_resources(api, '/projects', obj.get('projects', []))
+ self._create_resources(api, '/pipelines', obj.get('pipelines', []))
- def cmd_delete_pipeline(self, args):
- rc = self._new_rc('/pipelines', 'pipeline')
- name = args[0]
- rc.delete(name)
+ def _read_object(self):
+ return yaml.load(sys.stdin)
- def cmd_show_pipeline_status(self, args):
- self._prettyson(self._get_pipeline_status(args[0], args[1]))
+ def _create_resources(self, api, path, objs):
+ for obj in objs:
+ api.create(path, obj)
- def _get_pipeline_status(self, project, pipeline):
- path = '/projects/{}/pipelines/{}'.format(project, pipeline)
- api = self._new_api()
- code, text = api.get(path)
- self._report(code, 200, text)
- return json.loads(text)
-
- def _report(self, code, expected, text):
- if code != expected:
- sys.stderr.write('HTTP status {}\n'.format(code))
- sys.stderr.write(text)
- if not text.endswith('\n'):
- sys.stderr.write('\n')
- sys.exit(1)
-
- def cmd_set_pipeline(self, args):
- project = args[0]
- pipeline = args[1]
- state = args[2]
- path = '/projects/{}/pipelines/{}'.format(project, pipeline)
- api = self._new_api()
- code, text = api.put(path, {'status': state})
- self._report(code, 200, text)
- obj = json.loads(text)
- self._prettyson(obj)
-
- def cmd_trigger(self, args):
- project = args[0]
- pipeline = args[1]
- path = '/projects/{}/pipelines/{}/+trigger'.format(project, pipeline)
+ def cmd_show(self, args):
+ token = self._new_token()
api = self._new_api()
- code, text = api.get(path)
- self._report(code, 200, text)
- obj = json.loads(text)
- self._prettyson(obj)
-
- def cmd_list_workers(self, args):
- rc = self._new_rc('/workers', 'worker')
- self._prettyson(rc.list())
+ api.set_token(token)
+ if not args:
+ args = [
+ 'projects',
+ 'pipelines',
+ ]
- def cmd_create_worker(self, args):
- rc = self._new_rc('/workers', 'worker')
- obj = self._read_object()
- rc.create(obj)
+ for kind in args:
+ objs = api.show('/' + kind)
+ self._prettyson(objs)
- def cmd_update_worker(self, args):
- rc = self._new_rc('/workers', 'worker')
- obj = self._read_object()
- rc.update(obj)
-
- def cmd_show_worker(self, args):
- rc = self._new_rc('/workers', 'worker')
- name = args[0]
- self._prettyson(rc.show(name))
+ def _new_api(self):
+ api = ick2.ControllerClient()
+ api.set_verify_tls(self.settings['verify-tls'])
+ api.set_controller_url(self.settings['controller'])
+ return api
- def cmd_delete_worker(self, args):
- rc = self._new_rc('/workers', 'worker')
- name = args[0]
- rc.delete(name)
+ def _new_auth(self):
+ url = self.settings['auth-url']
+ client_id, client_secret = self._get_client_creds(url)
- def cmd_list_builds(self, args):
- self._prettyson(self._get_builds())
+ ac = ick2.AuthClient()
+ ac.set_auth_url(url)
+ ac.set_client_creds(client_id, client_secret)
+ return ac
- def cmd_list_logs(self, args):
- rc = self._new_rc('/logs', 'log')
- self._prettyson(rc.list())
+ def _new_token(self):
+ if self.settings['token']:
+ return self.settings['token']
+ ac = self._new_auth()
+ wanted_scopes = ' '.join(self.settings['scope'])
+ return ac.get_token(wanted_scopes)
+
+ def _get_client_creds(self, url):
+ cp = configparser.ConfigParser()
+ cp.read(self.settings['secrets'])
+ client_id = cp.get(url, 'client_id')
+ client_secret = cp.get(url, 'client_secret')
+ return client_id, client_secret
- def cmd_show_log(self, args):
- log = args[0]
- path = '/logs/{}'.format(log)
- api = self._new_api()
- code, text = api.get(path)
- self._report(code, 200, text)
- self.output.write(text)
-
- def cmd_show_latest_log(self, args):
- project = args[0]
- builds = self._get_builds()
- project_builds = [
- b
- for b in builds['builds']
- if b['project'] == project
- ]
- if project_builds:
- b = project_builds[-1]
- self.cmd_show_log([b['build_id']])
-
- def cmd_get_blob(self, args):
- blob_id = args[0]
- api = self._new_api()
- blob_api = self._new_blob_api(api)
- status_code, blob = blob_api.get(blob_id)
- if status_code == 200:
- filename = self.settings['output']
- with open(filename, 'wb') as f:
- f.write(blob)
- else:
- sys.exit('Error: {}'.format(status_code))
-
- def cmd_put_blob(self, args):
- blob_id = args[0]
- blob = sys.stdin.read()
- api = self._new_api()
- blob_api = self._new_blob_api(api)
- code, text = blob_api.put(blob_id, blob)
- if code != 200:
- sys.exit(text)
+ def _prettyson(self, obj):
+ json.dump(obj, self.output, indent=4, sort_keys=True)
+ self.output.write('\n')
- def _new_token(self):
- wanted_scopes = self.settings['scope']
- cmd = self.settings['token-private-key-cmd']
- if not cmd:
- raise cliapp.AppException('no --token-private-cmd specified')
-
- gen = TokenGenerator()
- gen.set_cmd(cmd)
- gen.set_scopes(wanted_scopes)
- token = gen.new_token()
- self.settings['token'] = token
- return token
- def _new_api(self):
- token = self.settings['token'] or self._new_token()
- api = API()
- api.set_token(token)
- api.set_url(self.settings['controller'])
- api.set_verify(self.settings['verify-tls'])
- return api
+class Command:
- def _new_blob_api(self, api):
- token = self.settings['token'] or self._new_token()
- blob_api = BlobAPI()
- blob_api.set_token(token)
- blob_api.set_url(api.get_artifact_store_url())
- blob_api.set_verify(self.settings['verify-tls'])
- return blob_api
+ def __init__(self, api):
+ self._api = api
- def _new_rc(self, path, field_name):
- api = self._new_api()
- return ResourceCommands(path, api, field_name)
+ def execute(self):
+ raise NotImplementedError()
- def _prettyson(self, obj):
- json.dump(obj, sys.stdout, indent=4, sort_keys=True)
- sys.stdout.write('\n')
- def _read_object(self):
- return yaml.load(sys.stdin)
+class VersionCommand(Command):
- def _pretty_table(self, rows, columns):
- headings = {
- column: column
- for column in columns
- }
-
- widths = {
- column: 0
- for column in columns
- }
-
- for row in [headings] + rows:
- for column in columns:
- widths[column] = max(widths[column], len(str(row[column])))
-
- underlines = {
- column: '-' * widths[column]
- for column in columns
- }
-
- for row in [headings, underlines] + rows:
- self.output.write(
- '{}\n'.format(self._pretty_row(widths, row, columns)))
-
- def _pretty_row(self, widths, row, columns):
- parts = ['%*s' % (widths[c], row[c]) for c in columns]
- return ' | '.join(parts)
-
-
-class API:
-
- def __init__(self):
- self._url = None
- self._token = None
- self._verify = None
-
- def set_url(self, url):
- self._url = url
-
- def set_token(self, token):
- self._token = token
-
- def set_verify(self, verify):
- self._verify = verify
-
- def get_version(self):
- code, text = self.get('/version')
- if code == 200:
- return json.loads(text)
-
- def get_artifact_store_url(self):
- version = self.get_version()
- if version:
- return version.get('artifact_store')
-
- def get(self, path):
- assert self._url is not None
- assert self._token is not None
-
- full_url = '{}/{}'.format(self._url, path)
- headers = {
- 'Authorization': 'Bearer {}'.format(self._token),
- }
- r = requests.get(full_url, headers=headers, verify=self._verify)
- return r.status_code, r.text
-
- def post(self, path, obj):
- assert self._url is not None
- assert self._token is not None
-
- full_url = '{}{}'.format(self._url, path)
- headers = {
- 'Authorization': 'Bearer {}'.format(self._token),
- }
- r = requests.post(
- full_url, json=obj, headers=headers, verify=self._verify)
- return r.status_code, r.text
-
- def put(self, path, obj):
- assert self._url is not None
- assert self._token is not None
-
- full_url = '{}{}'.format(self._url, path)
- headers = {
- 'Authorization': 'Bearer {}'.format(self._token),
- }
- r = requests.put(
- full_url, json=obj, headers=headers, verify=self._verify)
- return r.status_code, r.text
-
- def delete(self, path):
- assert self._url is not None
- assert self._token is not None
-
- full_url = '{}{}'.format(self._url, path)
- headers = {
- 'Authorization': 'Bearer {}'.format(self._token),
- }
- r = requests.delete(
- full_url, headers=headers, verify=self._verify)
- return r.status_code, r.text
-
-
-class BlobAPI:
-
- def __init__(self):
- self._url = None
- self._token = None
- self._verify = None
-
- def set_url(self, url):
- self._url = url
-
- def set_token(self, token):
- self._token = token
-
- def set_verify(self, verify):
- self._verify = verify
-
- def get(self, blob_id):
- assert self._url is not None
- assert self._token is not None
-
- full_url = '{}/blobs/{}'.format(self._url, blob_id)
- headers = {
- 'Authorization': 'Bearer {}'.format(self._token),
- }
- r = requests.get(full_url, headers=headers, verify=self._verify)
- print('blob length', len(r.content))
- return r.status_code, r.content
-
- def put(self, blob_id, blob):
- assert self._url is not None
- assert self._token is not None
-
- full_url = '{}/blobs/{}'.format(self._url, blob_id)
- headers = {
- 'Authorization': 'Bearer {}'.format(self._token),
- 'Content-Type': 'application/octet-stream',
- }
- r = requests.put(
- full_url, data=blob, headers=headers, verify=self._verify)
- return r.status_code, r.text
-
-
-class ResourceCommands:
-
- def __init__(self, path, api, name_field):
- self._path = path
- self._api = api
- self._name = name_field
-
- def list(self):
- code, text = self._api.get(self._path)
- self._report(code, 200, text)
- return json.loads(text)
-
- def create(self, obj):
- code, text = self._api.post(self._path, obj)
- self._report(code, 201, text)
-
- def update(self, obj):
- code, text = self._api.put(self._id_path(obj[self._name]), obj)
- self._report(code, 200, text)
-
- def show(self, name):
- code, text = self._api.get(self._id_path(name))
- self._report(code, 200, text)
- return json.loads(text)
-
- def exists(self, obj):
- code, text = self._api.get(self._id_path(obj[self._name]))
- return code == 200
-
- def delete(self, name):
- code, text = self._api.delete(self._id_path(name))
- self._report(code, 200, text)
-
- def _id_path(self, name):
- return '{}/{}'.format(self._path, name)
-
- def _report(self, code, expected, text):
- if code != expected:
- sys.stderr.write('HTTP status {}\n'.format(code))
- sys.stderr.write(text)
- if not text.endswith('\n'):
- sys.stderr.write('\n')
- sys.exit(1)
-
-
-class TokenGenerator:
-
- def __init__(self):
- self._cmd = None
- self._scopes = None
-
- def set_cmd(self, cmd):
- self._cmd = cmd
-
- def set_scopes(self, wanted_scopes):
- self._scopes = wanted_scopes
-
- def new_token(self):
- assert self._cmd is not None
- assert self._scopes is not None
-
- # These should agree with how ick controller is configured.
- # See the Ansible playbook. They should probably be
- # configurable.
- iss = 'localhost'
- aud = 'localhost'
-
- privkey = cliapp.runcmd(['sh', '-c', self._cmd])
- key = Crypto.PublicKey.RSA.importKey(privkey)
- wanted_scopes = ' '.join(self._scopes)
-
- now = time.time()
- claims = {
- 'iss': iss,
- 'sub': 'subject-uuid',
- 'aud': aud,
- 'exp': now + 86400,
- 'scope': wanted_scopes,
- }
-
- token = apifw.create_token(claims, key)
- return token.decode('ascii')
+ def execute(self):
+ self._api.get_version()
Icktool(version=ick2.__version__).run()
diff --git a/icktool2 b/icktool2
new file mode 100755
index 0000000..df5ccdd
--- /dev/null
+++ b/icktool2
@@ -0,0 +1,210 @@
+#!/usr/bin/python3
+# Copyright 2017-2018 Lars Wirzenius
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import configparser
+import json
+import logging
+import sys
+import time
+
+import apifw
+import cliapp
+import Crypto.PublicKey.RSA
+import requests
+import yaml
+
+import ick2
+
+
+def scopes(base):
+ patterns = [
+ 'uapi_{}_get',
+ 'uapi_{}_post',
+ 'uapi_{}_id_get',
+ 'uapi_{}_id_put',
+ 'uapi_{}_id_delete',
+ ]
+ return [x.format(base) for x in patterns]
+
+
+def scopes_for_types(typelist):
+ result = []
+ for type_name in typelist:
+ result.extend(scopes(type_name))
+ return result
+
+
+types = [
+ 'projects',
+ 'pipelines',
+ 'workers',
+ 'work',
+ 'builds',
+ 'logs',
+]
+
+
+class Icktool(cliapp.Application):
+
+ _default_scopes = [
+ 'uapi_version_get',
+ 'uapi_work_post',
+ 'uapi_projects_id_pipelines_id_get',
+ 'uapi_projects_id_pipelines_id_put',
+ 'uapi_blobs_id_get',
+ 'uapi_blobs_id_put',
+ ] + scopes_for_types(types)
+
+ def add_settings(self):
+ self.settings.string(
+ ['controller', 'c'],
+ 'use URL as the controller base URL',
+ metavar='URL',
+ )
+
+ self.settings.string(
+ ['auth-url'],
+ 'use URL as the authentication URL',
+ metavar='URL',
+ )
+
+ self.settings.string(
+ ['secrets'],
+ 'use URL as the controller base URL',
+ metavar='URL',
+ )
+
+ self.settings.boolean(
+ ['verify-tls'],
+ 'verify API provider TLS certificate '
+ '(default is verify, use --no-verify-tls)',
+ default=True,
+ )
+
+ self.settings.string(
+ ['token'],
+ 'use TOKEN instead of generating a new one',
+ metavar='TOKEN',
+ )
+
+ self.settings.string_list(
+ ['scope'],
+ 'add SCOPE to the list of scope in requested token',
+ metavar='SCOPE',
+ default=self._default_scopes,
+ )
+
+ def setup(self):
+ if not self.settings['verify-tls']:
+ logging.captureWarnings(True)
+
+ def cmd_scopes(self, args):
+ for scope in self.settings['scope']:
+ self.output.write('{}\n'.format(scope))
+
+ def cmd_token(self, args):
+ token = self._new_token()
+ self.output.write('{}\n'.format(token))
+
+ def cmd_version(self, args):
+ token = self._new_token()
+ api = self._new_api()
+ api.set_token(token)
+ version = api.get_version()
+ self._prettyson(version)
+
+ def cmd_make_it_so(self, argv):
+ obj = self._read_object()
+
+ token = self._new_token()
+ api = self._new_api()
+ api.set_token(token)
+
+ self._create_resources(api, '/projects', obj.get('projects', []))
+ self._create_resources(api, '/pipelines', obj.get('pipelines', []))
+
+ def _read_object(self):
+ return yaml.load(sys.stdin)
+
+ def _create_resources(self, api, path, objs):
+ for obj in objs:
+ api.create(path, obj)
+
+ def cmd_show(self, args):
+ token = self._new_token()
+ api = self._new_api()
+ api.set_token(token)
+ if not args:
+ args = [
+ 'projects',
+ 'pipelines',
+ ]
+
+ for kind in args:
+ objs = api.show('/' + kind)
+ self._prettyson(objs)
+
+ def _new_api(self):
+ api = ick2.ControllerClient()
+ api.set_verify_tls(self.settings['verify-tls'])
+ api.set_controller_url(self.settings['controller'])
+ return api
+
+ def _new_auth(self):
+ url = self.settings['auth-url']
+ client_id, client_secret = self._get_client_creds(url)
+
+ ac = ick2.AuthClient()
+ ac.set_auth_url(url)
+ ac.set_client_creds(client_id, client_secret)
+ return ac
+
+ def _new_token(self):
+ if self.settings['token']:
+ return self.settings['token']
+ ac = self._new_auth()
+ scopes = ' '.join(self.settings['scope'])
+ return ac.get_token(scopes)
+
+ def _get_client_creds(self, url):
+ cp = configparser.ConfigParser()
+ cp.read(self.settings['secrets'])
+ client_id = cp.get(url, 'client_id')
+ client_secret = cp.get(url, 'client_secret')
+ return client_id, client_secret
+
+ def _prettyson(self, obj):
+ json.dump(obj, self.output, indent=4, sort_keys=True)
+ self.output.write('\n')
+
+
+class Command:
+
+ def __init__(self, api):
+ self._api = api
+
+ def execute(self):
+ raise NotImplementedError()
+
+
+class VersionCommand(Command):
+
+ def execute(self):
+ self._api.get_version()
+
+
+Icktool(version=ick2.__version__).run()
diff --git a/pylint.conf b/pylint.conf
index 8ac3b99..ed09e29 100644
--- a/pylint.conf
+++ b/pylint.conf
@@ -10,6 +10,7 @@ disable=
no-self-use,
not-callable,
too-few-public-methods,
+ too-many-arguments,
too-many-public-methods,
unused-argument,
unused-variable
diff --git a/run-debug b/run-debug
index 3a4ae68..106ee19 100755
--- a/run-debug
+++ b/run-debug
@@ -1,5 +1,5 @@
#!/bin/sh
-# Copyright 2017 Lars Wirzenius
+# Copyright 2017-2018 Lars Wirzenius
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
diff --git a/worker_manager b/worker_manager
index fa92fa9..6fd556c 100755
--- a/worker_manager
+++ b/worker_manager
@@ -18,9 +18,7 @@
import logging
import time
-import apifw
import cliapp
-import Crypto.PublicKey.RSA
import urllib3
import ick2
@@ -34,7 +32,6 @@ class WorkerManager(cliapp.Application):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._token = None
- self._token_until = None
def add_settings(self):
self.settings.string(
@@ -49,18 +46,6 @@ class WorkerManager(cliapp.Application):
metavar='URL',
)
- self.settings.string(
- ['token-key'],
- 'get token signing private key from FILE',
- metavar='FILE',
- )
-
- self.settings.string(
- ['token-key-pub'],
- 'this is not used',
- metavar='NOPE',
- )
-
self.settings.integer(
['sleep'],
'sleep for SECS seconds if there is no work currently',
@@ -105,9 +90,7 @@ class WorkerManager(cliapp.Application):
workspace = self.settings['workspace']
systree = self.settings['systree']
- tg = TokenGenerator()
- tg.set_key(self.settings['token-key'])
- api = ControllerAPI(name, url, tg)
+ api = ControllerAPI(name, url)
api.set_verify_tls(self.settings['verify-tls'])
worker = Worker(name, api, workspace, systree)
@@ -128,18 +111,29 @@ class WorkerManager(cliapp.Application):
class ControllerAPI:
- def __init__(self, name, url, token_generator):
- self._token_generator = token_generator
+ _scopes = ' '.join([
+ 'uapi_version_get',
+ 'uapi_work_id_get',
+ 'uapi_work_post',
+ 'uapi_workers_post',
+ 'uapi_blobs_id_get',
+ 'uapi_blobs_id_put',
+ ])
+
+ def __init__(self, name, url):
self._cc = ick2.ControllerClient()
self._cc.set_client_name(name)
self._cc.set_controller_url(url)
+ self._ac = None
self._blobs = None
def set_verify_tls(self, verify):
self._cc.set_verify_tls(verify)
def get_token(self):
- return self._token_generator.get_token()
+ if self._ac is None:
+ self._ac = self._cc.get_auth_client()
+ return self._ac.get_token(self._scopes)
def register(self):
self._cc.set_token(self.get_token())
@@ -171,67 +165,6 @@ class ControllerAPI:
return self._blobs
-class TokenGenerator:
-
- max_age = 3600 # 1 hour
- sub = 'subject-uuid'
- iss = 'localhost'
- aud = 'localhost'
- scopes = ' '.join([
- 'uapi_version_get',
- 'uapi_work_id_get',
- 'uapi_work_post',
- 'uapi_workers_post',
- 'uapi_blobs_id_get',
- 'uapi_blobs_id_put',
- ])
-
- def __init__(self):
- self._token = None
- self._token_key = None
- self._token_until = None
-
- def is_valid(self, now):
- return (
- self._token is not None and
- (self._token_until is None or now <= self._token_until)
- )
-
- def set_token(self, token):
- self._token = token
- self._token_until = None
- assert self.is_valid(time.time())
-
- def set_key(self, filename):
- key_text = self.cat(filename)
- self._token_key = Crypto.PublicKey.RSA.importKey(key_text)
-
- def cat(self, filename):
- with open(filename) as f:
- return f.read()
-
- def get_token(self):
- now = time.time()
- if not self.is_valid(now):
- self._token = self.create_token()
- self._token_until = now + self.max_age
- assert self.is_valid(now)
- return self._token
-
- def create_token(self):
- now = time.time()
- claims = {
- 'iss': self.iss,
- 'sub': self.sub,
- 'aud': self.aud,
- 'exp': now + self.max_age,
- 'scope': self.scopes,
- }
-
- token = apifw.create_token(claims, self._token_key)
- return token.decode('ascii')
-
-
class Worker:
def __init__(self, name, api, workspace, systree):