From fd3fd66a1db8925e5a9a3d34acdd213f49378877 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 30 Apr 2018 17:24:27 +0300 Subject: Add: notification service --- debian/postinst | 1 + ick2/__init__.py | 3 ++ ick2/actions.py | 63 +++++++++++++++++++++- ick2/actions_tests.py | 5 +- ick2/client.py | 10 ++++ ick2/controllerapi.py | 3 ++ ick2/notificationapi.py | 45 ++++++++++++++++ ick2/sendmail.py | 92 +++++++++++++++++++++++++++++++ ick2/versionapi.py | 5 ++ ick2/versionapi_tests.py | 3 ++ ick2/workapi.py | 3 +- ick_controller.py | 2 + icktool | 1 + notification_service.py | 125 +++++++++++++++++++++++++++++++++++++++++++ notification_service.service | 29 ++++++++++ pylint.conf | 1 + send-notify-email | 41 ++++++++++++++ setup.py | 2 + start_notification_service | 20 +++++++ without-tests | 2 + worker_manager | 11 +++- yarns/100-projects.yarn | 1 + yarns/150-pipelines.yarn | 1 + yarns/200-version.yarn | 2 + yarns/300-workers.yarn | 1 + yarns/400-build.yarn | 3 ++ yarns/500-build-fail.yarn | 1 + yarns/600-unauthz.yarn | 1 + yarns/900-local.yarn | 14 +++++ 29 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 ick2/notificationapi.py create mode 100644 ick2/sendmail.py create mode 100644 notification_service.py create mode 100644 notification_service.service create mode 100755 send-notify-email create mode 100644 start_notification_service diff --git a/debian/postinst b/debian/postinst index 35578ed..7322fd5 100644 --- a/debian/postinst +++ b/debian/postinst @@ -37,6 +37,7 @@ create_user() create_user _ick /var/lib/ick /var/log/ick create_user _ickwm /var/lib/ick/wm /var/log/ickwm create_user _ickas /var/lib/ick/blobs /var/log/ickas +create_user _ickns /var/lib/ick/blobs /var/log/ickns # Create controller config dir. install -d -m 0755 -o root -g root /etc/ick diff --git a/ick2/__init__.py b/ick2/__init__.py index a6a4295..fe8027c 100644 --- a/ick2/__init__.py +++ b/ick2/__init__.py @@ -71,6 +71,7 @@ from .workerapi import WorkerAPI from .controllerapi import ( ControllerAPI, ) +from .notificationapi import NotificationAPI from .blobapi import BlobAPI from .blob_store import BlobStore @@ -101,3 +102,5 @@ from .actions import ( PopulateSystreeAction, GitAction, ) + +from .sendmail import Sendmail diff --git a/ick2/actions.py b/ick2/actions.py index 202bcb6..3d77aa5 100644 --- a/ick2/actions.py +++ b/ick2/actions.py @@ -35,7 +35,9 @@ class ActionFactory: 'container': ick2.ContainerEnvironment, } - def __init__(self, systree, workspace_area, reporter): + def __init__(self, build_id, systree, workspace_area, reporter): + self._cc = None + self._build_id = build_id self._systree = systree self._workspace_area = workspace_area self._reporter = reporter @@ -46,6 +48,9 @@ class ActionFactory: def add_env_var(self, name, value): # pragma: no cover self._extra_env[name] = value + def set_controller_client(self, cc): # pragma: no cover + self._cc = cc + def set_token(self, token): self._token = token @@ -79,6 +84,8 @@ class ActionFactory: def create_action(self, spec, project_name): env = self.create_environment(spec, project_name) action = self._create_action_object(env, spec) + action.set_controller_client(self._cc) + action.set_build_id(self._build_id) action.set_token(self.get_token()) action.set_blob_url_func(self.get_blob_url_func()) return action @@ -103,6 +110,7 @@ class ActionFactory: 'git': GitAction, 'rsync': RsyncAction, 'dput': DputAction, + 'notify': NotifyAction, } kind = spec['action'] klass = rules2.get(kind) @@ -116,9 +124,23 @@ class Action: # pragma: no cover def __init__(self, env): self._env = env + self._cc = None + self._build_id = None self._token = None self._blob_url = None + def set_controller_client(self, cc): + self._cc = cc + + def get_controller_client(self): + return self._cc + + def set_build_id(self, build_id): + self._build_id = build_id + + def get_build_id(self): + return self._build_id + def set_token(self, token): self._token = token @@ -444,6 +466,45 @@ class DputAction(Action): # pragma: no cover return exit_code +class NotifyAction(Action): # pragma: no cover + + def encode_parameters(self, params): + pass + + def execute(self, params, step): + env = self.get_env() + cc = self.get_controller_client() + assert cc is not None + build_id = self.get_build_id() + + env.report(None, 'Notifying about build ending\n') + + build_path = '/builds/{}'.format(build_id) + build = cc.show(build_path) + + params = build.get('parameters', {}) + if 'notify' not in params: + env.report( + 0, + 'NOT notifying about build ending: no "notify" parameter.\n') + return + + recipients = params['notify'] + + log = cc.get_log(build_id) + log = log.decode('utf-8') + + notify = { + 'recipients': recipients, + 'build': build, + 'log': log, + } + + cc.notify(notify) + + env.report(0, 'Notified about build {} ending\n'.format(build_id)) + + def make_directory_empty(env, dirname): return env.runcmd( ['sudo', 'find', dirname, '-mindepth', '1', '-delete']) diff --git a/ick2/actions_tests.py b/ick2/actions_tests.py index 06c07f5..775428f 100644 --- a/ick2/actions_tests.py +++ b/ick2/actions_tests.py @@ -28,7 +28,9 @@ class ActionFactoryTests(unittest.TestCase): self.tempdir = tempfile.mkdtemp() self.workspace_area = self.tempdir self.project = 'magic' - self.af = ick2.ActionFactory('systree', self.workspace_area, None) + self.build_id = 'moomin/42' + self.af = ick2.ActionFactory( + self.build_id, 'systree', self.workspace_area, None) def tearDown(self): shutil.rmtree(self.tempdir) @@ -88,6 +90,7 @@ class ActionFactoryTests(unittest.TestCase): action = self.af.create_action(spec, self.project) self.assertTrue(isinstance(action, ick2.ShellAction)) self.assertTrue(isinstance(action.get_env(), ick2.HostEnvironment)) + self.assertEqual(action.get_build_id(), self.build_id) self.assertEqual(action.get_token(), token) self.assertEqual(action.get_blob_upload_url('foo'), 'foo') diff --git a/ick2/client.py b/ick2/client.py index 47941b5..e654d29 100644 --- a/ick2/client.py +++ b/ick2/client.py @@ -172,6 +172,12 @@ class ControllerClient: logging.info('Authentication URL: %r', url) return url + def get_notify_url(self): # pragma: no cover + version = self.get_version() + url = version.get('notify_url') + logging.info('Notification URL: %r', url) + return url + def get_auth_client(self): url = self.get_auth_url() ac = AuthClient() @@ -249,6 +255,10 @@ class ControllerClient: url = self.url(path) return self._api.get_blob(url) + def notify(self, notify): # pragma: no cover + url = self.get_notify_url() + self._api.post(url, body=notify) + class AuthClient: diff --git a/ick2/controllerapi.py b/ick2/controllerapi.py index c2e7eff..1bc830d 100644 --- a/ick2/controllerapi.py +++ b/ick2/controllerapi.py @@ -28,6 +28,9 @@ class ControllerAPI: def set_auth_url(self, url): # pragma: no cover self._set_url('set_auth_url', url) + def set_notify_url(self, url): # pragma: no cover + self._set_url('set_notify_url', url) + def _set_url(self, what, url): # pragma: no cover api = self._get_version_api() if api: diff --git a/ick2/notificationapi.py b/ick2/notificationapi.py new file mode 100644 index 0000000..ab261bd --- /dev/null +++ b/ick2/notificationapi.py @@ -0,0 +1,45 @@ +# Copyright 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 . + + +import ick2 + + +class NotificationAPI: + + def __init__(self, config): + self._config = config + + def find_missing_route(self, missing_path): + return [ + { + 'method': 'POST', + 'path': '/notify', + 'callback': self.notify, + }, + ] + + def notify(self, content_type, body, **kwargs): + ick2.log.log('info', msg_text='Notification requested', kwargs=kwargs) + + recipients = body.get('recipients', []) + build = body.get('build', {}) + log = body.get('log', '') + + sendmail = ick2.Sendmail() + sendmail.set_config(self._config) + sendmail.send(recipients, build, log) + + return ick2.OK('') diff --git a/ick2/sendmail.py b/ick2/sendmail.py new file mode 100644 index 0000000..0b0d34e --- /dev/null +++ b/ick2/sendmail.py @@ -0,0 +1,92 @@ +# Copyright 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 . + + +import email.message +import smtplib + + +body_tmpl = """\ +This is a notification from ick, the CI system. + +Project: {project} +Build ID: {build_id} +Log ID: {log} +Worker: {worker} +Status: {status} +Exit code: {exit_code} + +Build log is attached. + +""" + + +class Sendmail: + + config_fields = [ + 'from_addr', + 'smtp_server', + 'smtp_port', + 'smtp_user', + 'smtp_password', + ] + + def __init__(self): + self._config = None + + def set_config(self, config): + for field in self.config_fields: + assert field in config + self._config = config + + def send(self, recipients, build, log): + msg = self.construct_msg(recipients, build, log) + self._send_msg(recipients, msg) + + def _send_msg(self, recipients, msg): + server = smtplib.SMTP() + server.connect( + host=self._config['smtp_server'], port=self._config['smtp_port']) + server.ehlo() + server.starttls() + server.login(self._config['smtp_user'], self._config['smtp_password']) + server.sendmail(self._config['from_addr'], recipients, msg.as_bytes()) + + def construct_msg(self, recipients, build, log): + msg = email.message.Message() + msg.add_header('From', self._config['from_addr']) + msg.add_header('To', ', '.join(recipients)) + msg.add_header('Subject', self.get_subject(build)) + msg.add_header('Content-Type', 'multipart/mixed') + msg.attach(self.get_body(build)) + msg.attach(self.get_log(log)) + return msg + + def get_subject(self, build): + return 'Ick notification: {build_id} {status}'.format(**build) + + def get_body(self, build): + msg = email.message.Message() + msg.set_payload(body_tmpl.format(**build)) + msg.add_header('Content-Type', 'text/plain') + return msg + + def get_log(self, log): + msg = email.message.Message() + msg.set_payload(log) + msg.add_header('Content-Type', 'text/plain') + msg.add_header( + 'Content-Disposition', 'inline', filename='build.log') + return msg diff --git a/ick2/versionapi.py b/ick2/versionapi.py index c688c1b..cd62ce9 100644 --- a/ick2/versionapi.py +++ b/ick2/versionapi.py @@ -22,6 +22,7 @@ class VersionAPI(ick2.APIbase): super().__init__(state) self._artifact_store_url = None self._auth_url = None + self._notify_url = None def set_artifact_store_url(self, url): self._artifact_store_url = url @@ -29,6 +30,9 @@ class VersionAPI(ick2.APIbase): def set_auth_url(self, url): self._auth_url = url + def set_notify_url(self, url): + self._notify_url = url + def get_routes(self, path): # pragma: no cover return [ { @@ -44,6 +48,7 @@ class VersionAPI(ick2.APIbase): 'version': ick2.__version__, 'artifact_store': self._artifact_store_url, 'auth_url': self._auth_url, + 'notify_url': self._notify_url, } def create(self, body, **kwargs): # pragma: no cover diff --git a/ick2/versionapi_tests.py b/ick2/versionapi_tests.py index 32f9808..4f42b29 100644 --- a/ick2/versionapi_tests.py +++ b/ick2/versionapi_tests.py @@ -24,9 +24,11 @@ class VersionAPITests(unittest.TestCase): def test_returns_version_correcly(self): bloburl = 'https://blobs.example.com' idpurl = 'https://idp.example.com' + notifyurl = 'https://notify.example.com' api = ick2.VersionAPI(None) api.set_artifact_store_url(bloburl) api.set_auth_url(idpurl) + api.set_notify_url(notifyurl) response = api.get_version() self.assertEqual( response, @@ -34,5 +36,6 @@ class VersionAPITests(unittest.TestCase): 'version': ick2.__version__, 'artifact_store': bloburl, 'auth_url': idpurl, + 'notify_url': notifyurl, } ) diff --git a/ick2/workapi.py b/ick2/workapi.py index 01f9932..0c885b5 100644 --- a/ick2/workapi.py +++ b/ick2/workapi.py @@ -192,12 +192,13 @@ class WorkAPI(ick2.APIbase): name, doing.get(name), update[name])) def _append_to_build_log(self, log, update): + ick2.log.log('trace', msg_text='appending to build log', update=update) for stream in ['stdout', 'stderr']: text = update.get(stream, '') self._append_text_to_build_log(log, text) def _append_text_to_build_log(self, log, text): - log['log'] = log.get('log', '') + text + log['log'] = log.get('log', '') + (text or '') def create(self, body, **kwargs): # pragma: no cover pass diff --git a/ick_controller.py b/ick_controller.py index 108a22a..4c952af 100644 --- a/ick_controller.py +++ b/ick_controller.py @@ -48,6 +48,7 @@ default_config = { 'statedir': None, 'artifact-store': None, 'auth-url': None, + 'notify-url': None, } @@ -97,6 +98,7 @@ def main(): api = ick2.ControllerAPI(state) api.set_artifact_store_url(config['artifact-store']) api.set_auth_url(config['auth-url']) + api.set_notify_url(config['notify-url']) ick2.log.log( 'info', msg_text='created ControllerAPI', artifact_store=config['artifact-store']) diff --git a/icktool b/icktool index 2fcdf04..3e954e2 100755 --- a/icktool +++ b/icktool @@ -66,6 +66,7 @@ class Icktool(cliapp.Application): 'uapi_projects_id_status_put', 'uapi_blobs_id_get', 'uapi_blobs_id_put', + 'uapi_notify_post', ] + scopes_for_types(types) def add_settings(self): diff --git a/notification_service.py b/notification_service.py new file mode 100644 index 0000000..002650d --- /dev/null +++ b/notification_service.py @@ -0,0 +1,125 @@ +#!/usr/bin/python3 +# Copyright (C) 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 . + + +import logging +import logging.handlers +import os +import sys + + +import apifw +import slog +import yaml + + +import ick2 + + +transactions = slog.Counter() + + +def counter(): + return 'HTTP transaction {}'.format(transactions.increment()) + + +def dict_logger(log, stack_info=None): + ick2.log.log(exc_info=stack_info, **log) + + +default_config = { + 'token-public-key': None, + 'token-audience': None, + 'token-issuer': None, + 'log': [], + 'from_addr': None, + 'smtp_server': None, + 'smtp_port': None, + 'smtp_user': None, + 'smtp_password': None +} + + +def load_config(filename, defconf): + conf = yaml.safe_load(open(filename, 'r')) + actual_config = dict(defconf) + actual_config.update(conf) + return actual_config + + +def check_config(config, musthave): + for key in config: + if key not in musthave: + raise Exception('Config %s is not known' % key) + for key in musthave: + if config.get(key) is None: + raise Exception('Config %s must not be None' % key) + + +def main(): + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler(sys.stderr) + logger.addHandler(handler) + logging.info('Starting notification service') + + try: + + name = 'NOTIFICATION_SERVICE_CONFIG' + config_filename = os.environ.get(name) + logging.info('config filename %r', config_filename) + if not config_filename: + msg = 'No %s defined in environment' % name + logging.error(msg) + raise Exception(msg) + + logging.info('reading config from %r', config_filename) + config = load_config(config_filename, default_config) + + logging.info('config is %r', config) + ick2.setup_logging(config) + check_config(config, default_config) + + ick2.log.log( + 'info', msg_text='Notification service starts', config=config) + + api = ick2.NotificationAPI(config) + ick2.log.log('info', msg_text='created NotificationAPI') + + application = apifw.create_bottle_application( + api, counter, dict_logger, config) + ick2.log.log('info', msg_text='called apifw.create_bottle_application') + + return application + except SystemExit: + raise + except BaseException as e: + logging.error(str(e)) + ick2.log.log( + 'error', msg_text='Uncaught exception', exception=str(e), + exc_info=True) + sys.exit(1) + + +app = main() + +# If we are running this program directly with Python, and not via +# gunicorn, we can use the Bottle built-in debug server, which can +# make some things easier to debug. + +if __name__ == '__main__': + print('running in debug mode') + app.run(host='127.0.0.1', port=12767) diff --git a/notification_service.service b/notification_service.service new file mode 100644 index 0000000..16f8488 --- /dev/null +++ b/notification_service.service @@ -0,0 +1,29 @@ +# Copyright 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 . + +[Unit] +Description=ick notification service +After=network.target +ConditionPathExists=/etc/ick/notifications.yaml + +[Service] +Type=simple +User=_ickns +Group=_ickns +WorkingDirectory=/var/lib/ick +ExecStart=/usr/bin/start_notification_service +KillSignal=QUIT + +[Install] +WantedBy=multi-user.target diff --git a/pylint.conf b/pylint.conf index 8a3d3a8..8041750 100644 --- a/pylint.conf +++ b/pylint.conf @@ -12,6 +12,7 @@ disable= redefined-outer-name, too-few-public-methods, too-many-arguments, + too-many-instance-attributes, too-many-locals, too-many-public-methods, unused-argument, diff --git a/send-notify-email b/send-notify-email new file mode 100755 index 0000000..4e134e1 --- /dev/null +++ b/send-notify-email @@ -0,0 +1,41 @@ +#!/usr/bin/python3 +# Copyright (C) 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 . + +# This is a helper program to manually test the code to send +# notification emails. Run with four args: +# +# $0 config recipients build log +# +# where all but the log are JSON files. See ick2/sendmail.py for +# details. + + +import json +import sys + + +import ick2 + + +config = json.load(open(sys.argv[1])) +recipients = json.load(open(sys.argv[2])) +build = json.load(open(sys.argv[3])) +log = open(sys.argv[4]).read() + + +sendmail = ick2.Sendmail() +sendmail.set_config(config) +sendmail.send(recipients, build, log) diff --git a/setup.py b/setup.py index 70b684a..39a7977 100644 --- a/setup.py +++ b/setup.py @@ -49,12 +49,14 @@ setup( py_modules=[ 'ick_controller', 'artifact_store', + 'notification_service', ], packages=['ick2'], scripts=[ 'create-token', 'start_ick', 'start_artifact_store', + 'start_notification_service', 'icktool', 'worker_manager' ], diff --git a/start_notification_service b/start_notification_service new file mode 100644 index 0000000..c35ef60 --- /dev/null +++ b/start_notification_service @@ -0,0 +1,20 @@ +#!/bin/sh +# Copyright (C) 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 . + +set -eux + +export NOTIFICATION_SERVICE_CONFIG=/etc/ick/notifications.yaml +python3 /usr/lib/python3/dist-packages/notification_service.py diff --git a/without-tests b/without-tests index 1014317..0e24420 100644 --- a/without-tests +++ b/without-tests @@ -7,8 +7,10 @@ ick2/controllerapi.py ick2/exceptions.py ick2/logapi.py ick2/logging.py +ick2/notificationapi.py ick2/pipelineapi.py ick2/responses.py ick2/trans.py +ick2/sendmail.py ick2/version.py ick2/workerapi.py diff --git a/worker_manager b/worker_manager index d0ee145..ab267d5 100755 --- a/worker_manager +++ b/worker_manager @@ -129,11 +129,14 @@ class ControllerAPI: _scopes = ' '.join([ 'uapi_version_get', + 'uapi_notify_post', 'uapi_work_get', 'uapi_work_post', 'uapi_workers_post', 'uapi_blobs_id_get', 'uapi_blobs_id_put', + 'uapi_builds_id_get', + 'uapi_logs_id_get', ]) def __init__(self, name, url): @@ -145,6 +148,9 @@ class ControllerAPI: self._client_id = None self._client_secret = None + def get_controller_client(self): + return self._cc + def set_verify_tls(self, verify): self._cc.set_verify_tls(verify) @@ -199,10 +205,13 @@ class Worker: def do_work(self, work): project_name = work['project'] build_number = work['build_number'] + build_id = work['build_id'] step = work.get('step', {}) params = work.get('parameters', {}) reporter = ick2.Reporter(self._api, work) - af = ick2.ActionFactory(self._systree, self._workspace, reporter) + af = ick2.ActionFactory( + build_id, self._systree, self._workspace, reporter) + af.set_controller_client(self._api.get_controller_client()) af.set_token(self._api.get_token()) af.set_blob_url_func(self._api.get_blob_upload_url) af.add_env_var('LC_ALL', 'C') diff --git a/yarns/100-projects.yarn b/yarns/100-projects.yarn index c5052f9..2fddded 100644 --- a/yarns/100-projects.yarn +++ b/yarns/100-projects.yarn @@ -57,6 +57,7 @@ building them. We start by starting an instance of the controller. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND a running ick controller WHEN user makes request GET /projects diff --git a/yarns/150-pipelines.yarn b/yarns/150-pipelines.yarn index a63ce02..e2416a8 100644 --- a/yarns/150-pipelines.yarn +++ b/yarns/150-pipelines.yarn @@ -68,6 +68,7 @@ running them. We start by starting an instance of the controller. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND a running ick controller WHEN user makes request GET /pipelines diff --git a/yarns/200-version.yarn b/yarns/200-version.yarn index 8d8078d..710a57a 100644 --- a/yarns/200-version.yarn +++ b/yarns/200-version.yarn @@ -28,6 +28,7 @@ The Ick controller reports is version upon request. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND a running ick controller WHEN user makes request GET /version @@ -35,6 +36,7 @@ The Ick controller reports is version upon request. AND version in body matches version from setup.py AND artifact store URL is https://blobs.example.com AND authentication URL is https://auth.example.com + AND notify URL is https://notify.example.com FINALLY stop ick controller diff --git a/yarns/300-workers.yarn b/yarns/300-workers.yarn index 89444f9..6399b20 100644 --- a/yarns/300-workers.yarn +++ b/yarns/300-workers.yarn @@ -63,6 +63,7 @@ controller API. It doesn't actually talk to the worker itself. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND a running ick controller WHEN user makes request GET /workers diff --git a/yarns/400-build.yarn b/yarns/400-build.yarn index 1674189..cef92e9 100644 --- a/yarns/400-build.yarn +++ b/yarns/400-build.yarn @@ -29,6 +29,7 @@ Set up the controller. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND an access token for user with scopes ... uapi_pipelines_post ... uapi_projects_post @@ -820,6 +821,7 @@ Set up the controller. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND an access token for user with scopes ... uapi_pipelines_post ... uapi_projects_post @@ -1027,6 +1029,7 @@ Set up the controller. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND an access token for user with scopes ... uapi_pipelines_post ... uapi_projects_post diff --git a/yarns/500-build-fail.yarn b/yarns/500-build-fail.yarn index 204bfd5..3373c2f 100644 --- a/yarns/500-build-fail.yarn +++ b/yarns/500-build-fail.yarn @@ -30,6 +30,7 @@ Set up the controller. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND an access token for user with scopes ... uapi_pipelines_post ... uapi_projects_post diff --git a/yarns/600-unauthz.yarn b/yarns/600-unauthz.yarn index c656f0e..1c928ac 100644 --- a/yarns/600-unauthz.yarn +++ b/yarns/600-unauthz.yarn @@ -30,6 +30,7 @@ Set up the controller. AND controller config uses statedir at the state directory AND controller config uses https://blobs.example.com as artifact store AND controller config uses https://auth.example.com as authentication + AND controller config uses https://notify.example.com as notify AND an access token for user with scopes ... uapi_projects_post ... uapi_projects_id_status_put diff --git a/yarns/900-local.yarn b/yarns/900-local.yarn index 40bf762..1318eb9 100644 --- a/yarns/900-local.yarn +++ b/yarns/900-local.yarn @@ -53,6 +53,10 @@ along with this program. If not, see . IMPLEMENTS GIVEN controller config uses (\S+) as authentication vars['auth_url'] = get_next_match() + IMPLEMENTS GIVEN controller config uses (\S+) as notify + vars['notify_url'] = get_next_match() + assert vars['notify_url'] is not None + ## Start and stop the controller IMPLEMENTS GIVEN a running ick controller @@ -62,6 +66,7 @@ along with this program. If not, see . vars['port'] = random_free_port() vars['url'] = 'http://127.0.0.1:{}'.format(vars['port']) assert vars['auth_url'] is not None + assert vars['notify_url'] is not None config = { 'token-issuer': vars['issuer'], 'token-audience': vars['audience'], @@ -74,7 +79,9 @@ along with this program. If not, see . 'statedir': vars['statedir'], 'artifact-store': vars['artifact_store'], 'auth-url': vars['auth_url'], + 'notify-url': vars['notify_url'], } + assert config['notify-url'] is not None env = dict(os.environ) env['ICK_CONTROLLER_CONFIG'] = 'ick_controller.yaml' yaml.safe_dump(config, open('ick_controller.yaml', 'w')) @@ -131,6 +138,13 @@ along with this program. If not, see . actual = obj['auth_url'] assertEqual(actual, expected) + IMPLEMENTS THEN notify URL is (\S+) + expected = get_next_match() + body = vars['body'] + obj = json.loads(body) + actual = obj['notify_url'] + assertEqual(actual, expected) + ## Start and stop artifact store IMPLEMENTS GIVEN artifact store config uses (\S+) at the blob directory -- cgit v1.2.1