diff --git a/.gitignore b/.gitignore index 6ff0c709..424aaf9d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ __pycache__/ # Sphinx documentation .doctrees/ -docs/* +docs/**/ !docs/src # Datasets diff --git a/README.md b/README.md index 1fef46c1..c8adc6f3 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ Prerequisites: MacOS or Linux environment bash scripts/start.sh ``` -To destroy Rafiki's complete stack: +To completely destroy Rafiki's stack: - ```sh - bash scripts/stop.sh - ``` +```sh +bash scripts/stop.sh +``` More instructions are available in [Rafiki's Developer Guide](https://nginyc.github.io/rafiki/docs/latest/docs/src/dev). diff --git a/conf.py b/docs/conf.py similarity index 100% rename from conf.py rename to docs/conf.py diff --git a/index.rst b/docs/index.rst similarity index 95% rename from index.rst rename to docs/index.rst index f1bdb11f..1c30f81b 100644 --- a/index.rst +++ b/docs/index.rst @@ -12,9 +12,9 @@ Index .. toctree:: :maxdepth: 2 - docs/src/user/index.rst - docs/src/dev/index.rst - docs/src/python/index.rst + src/user/index.rst + src/dev/index.rst + src/python/index.rst What is Rafiki? -------------------------------------------------------------------- @@ -33,7 +33,6 @@ For *Model Developers*, they can: - Contribute to Rafiki's pool of model templates - Check out :ref:`quick-setup` to deploy/develop Rafiki on your machine, and/or :ref:`quick-start` to use a deployed instance of Rafiki. Issues diff --git a/docs/src/dev/development.rst b/docs/src/dev/development.rst index 8c94480f..2a9bc5ef 100644 --- a/docs/src/dev/development.rst +++ b/docs/src/dev/development.rst @@ -13,6 +13,18 @@ Before running any individual scripts, make sure to run the shell configuration Refer to :ref:`architecture` and :ref:`folder-structure` for a developer's overview of Rafiki. +Building Images Locally +-------------------------------------------------------------------- + +The quickstart instructions pull pre-built `Rafiki's images `_ from Docker Hub. To build Rafiki's images locally (e.g. to reflect latest code changes): + + .. code-block:: shell + + bash scripts/build_images.sh + +.. note:: + + If you're testing latest code changes on multiple nodes, you'll need to build Rafiki's images on those nodes as well. Starting Parts of the Stack -------------------------------------------------------------------- @@ -49,19 +61,6 @@ You can connect to Redis DB with `rebrow `_: RAFIKI_ADDR=127.0.0.1 REDIS_EXT_PORT=6380 -Building Images Locally --------------------------------------------------------------------- - -The quickstart instructions pull pre-built `Rafiki's images `_ from Docker Hub. To build Rafiki's images locally (e.g. to reflect latest code changes): - - .. code-block:: shell - - bash scripts/build_images.sh - -.. note:: - - If you're testing latest code changes on multiple nodes, you'll need to build Rafiki's images on those nodes as well. - Pushing Images to Docker Hub -------------------------------------------------------------------- diff --git a/docs/src/dev/setup.rst b/docs/src/dev/setup.rst index fc8ade4c..cf7f4c80 100644 --- a/docs/src/dev/setup.rst +++ b/docs/src/dev/setup.rst @@ -16,6 +16,10 @@ We assume development or deployment in a MacOS or Linux environment. 1. Install Docker 18 (`Ubuntu `__, `MacOS `__) and, if required, add your user to ``docker`` group (`Linux `__). +.. note:: + + If you're not a user in the ``docker`` group, you'll instead need ``sudo`` access and prefix every bash command with ``sudo -E``. + 2. Install Python 3.6 (`Ubuntu `__, `MacOS `__) 3. Clone the project at https://github.com/nginyc/rafiki (e.g. with `Git `__) diff --git a/docs/src/user/quickstart-admins.rst b/docs/src/user/quickstart-admins.rst index d0cb8618..201ea4b7 100644 --- a/docs/src/user/quickstart-admins.rst +++ b/docs/src/user/quickstart-admins.rst @@ -40,16 +40,64 @@ Examples: .. code-block:: python client.create_user( - email='app_developer@rafiki', + email='admin@rafiki', password='rafiki', - user_type='APP_DEVELOPER' + user_type='ADMIN' ) - + client.create_user( email='model_developer@rafiki', password='rafiki', user_type='MODEL_DEVELOPER' ) + client.create_user( + email='app_developer@rafiki', + password='rafiki', + user_type='APP_DEVELOPER' + ) + + +.. seealso:: :meth:`rafiki.client.Client.create_user` + + +Listing all users +-------------------------------------------------------------------- + +Example: + + .. code-block:: python + + client.get_users() + + + .. code-block:: python + + [{'email': 'superadmin@rafiki', + 'id': 'c815fa08-ce06-467d-941b-afc27684d092', + 'user_type': 'SUPERADMIN'}, + {'email': 'admin@rafiki', + 'id': 'cb2c0d61-acd3-4b65-a5a7-d78aa5648283', + 'user_type': 'ADMIN'}, + {'email': 'model_developer@rafiki', + 'id': 'bfe58183-9c69-4fbd-a7b3-3fdc267b3290', + 'user_type': 'MODEL_DEVELOPER'}, + {'email': 'app_developer@rafiki', + 'id': '958a7d65-aa1d-437f-858e-8837bb3ecf32', + 'user_type': 'APP_DEVELOPER'}] + + +.. seealso:: :meth:`rafiki.client.Client.get_users` + + +Banning a user +-------------------------------------------------------------------- + +Example: + + .. code-block:: python + + client.ban_user('app_developer@rafiki') + -.. seealso:: :meth:`rafiki.client.Client.create_user` \ No newline at end of file +.. seealso:: :meth:`rafiki.client.Client.ban_user` \ No newline at end of file diff --git a/examples/scripts/seed_users.py b/examples/scripts/seed_users.py index 177409cc..ab79efbe 100644 --- a/examples/scripts/seed_users.py +++ b/examples/scripts/seed_users.py @@ -1,12 +1,23 @@ import pprint import os +import csv from rafiki.client import Client from rafiki.config import SUPERADMIN_EMAIL, SUPERADMIN_PASSWORD -def seed_users(client): - users = client.create_users('examples/seeds/users.csv') - pprint.pprint(users) +def seed_users(client, csv_file_path): + with open(csv_file_path, 'rt', encoding='utf-8-sig') as f: + reader = csv.DictReader(f) + reader.fieldnames = [name.lower() for name in reader.fieldnames] + for row in reader: + email = row['email'] + password = row['password'] + user_type = row['user_type'] + try: + client.create_user(email, password, user_type) + except Exception as e: + print('Failed to create user `{}` due to:'.format(email)) + print(e) if __name__ == '__main__': rafiki_host = os.environ.get('RAFIKI_HOST', 'localhost') @@ -14,9 +25,10 @@ def seed_users(client): admin_web_port = int(os.environ.get('ADMIN_WEB_EXT_PORT', 3001)) user_email = os.environ.get('USER_EMAIL', SUPERADMIN_EMAIL) user_password = os.environ.get('USER_PASSWORD', SUPERADMIN_PASSWORD) + csv_file_path = os.environ.get('CSV_FILE_PATH', 'examples/scripts/users.csv') # Initialize client client = Client(admin_host=rafiki_host, admin_port=admin_port) client.login(email=user_email, password=user_password) - seed_users(client) \ No newline at end of file + seed_users(client, csv_file_path) \ No newline at end of file diff --git a/examples/scripts/users.csv b/examples/scripts/users.csv new file mode 100644 index 00000000..05c9d8ad --- /dev/null +++ b/examples/scripts/users.csv @@ -0,0 +1,4 @@ +EMAIL,PASSWORD,USER_TYPE +admin@rafiki,rafiki,ADMIN +model_developer@rafiki,rafiki,MODEL_DEVELOPER +app_developer@rafiki,rafiki,APP_DEVELOPER \ No newline at end of file diff --git a/examples/seeds/users.csv b/examples/seeds/users.csv deleted file mode 100644 index ae5c8626..00000000 --- a/examples/seeds/users.csv +++ /dev/null @@ -1,7 +0,0 @@ -EMAIL,PASSWORD,USER_TYPE -john@rafiki,abcde,MODEL_DEVELOPER -sarah@rafiki,fghij,MODEL_DEVELOPER -aaron@rafiki,klmnop,MODEL_DEVELOPER -diance@rafiki,qrstuv,MODEL_DEVELOPER -peter@rafiki,wxyz,MODEL_DEVELOPER -watson@rafiki,12345,MODEL_DEVELOPER \ No newline at end of file diff --git a/rafiki/admin/admin.py b/rafiki/admin/admin.py index f1e98d28..21f32fbb 100644 --- a/rafiki/admin/admin.py +++ b/rafiki/admin/admin.py @@ -3,7 +3,6 @@ import traceback import bcrypt import uuid -import csv from rafiki.db import Database from rafiki.constants import ServiceStatus, UserType, ServiceType, InferenceJobStatus, \ @@ -17,6 +16,7 @@ logger = logging.getLogger(__name__) class UserExistsError(Exception): pass +class UserAlreadyBannedError(Exception): pass class InvalidUserError(Exception): pass class InvalidPasswordError(Exception): pass class InvalidRunningInferenceJobError(Exception): pass @@ -42,7 +42,7 @@ def __init__(self, db=None, container_manager=None): def seed(self): with self._db: - self._seed_users() + self._seed_superadmin() #################################### # Users @@ -59,39 +59,59 @@ def authenticate_user(self, email, password): return { 'id': user.id, - 'user_type': user.user_type + 'email': user.email, + 'user_type': user.user_type, + 'banned_date': user.banned_date } def create_user(self, email, password, user_type): user = self._create_user(email, password, user_type) return { - 'id': user.id + 'id': user.id, + 'email': user.email, + 'user_type': user.user_type } - def create_users(self, csv_file_bytes): - temp_csv_file = '{}.csv'.format(str(uuid.uuid4())) - - # Temporarily save the csv file to disk - with open(temp_csv_file, 'wb') as f: - f.write(csv_file_bytes) - - users = [] - with open(temp_csv_file, 'rt', encoding='utf-8-sig') as f: - reader = csv.DictReader(f) - reader.fieldnames = [name.lower() for name in reader.fieldnames] - for row in reader: - user = self._create_user(row['email'], row['password'], row['user_type']) - users.append(user) - os.remove(temp_csv_file) + def get_users(self): + users = self._db.get_users() return [ { 'id': user.id, 'email': user.email, - 'user_type': user.user_type + 'user_type': user.user_type, + 'banned_date': user.banned_date } for user in users ] + def get_user_by_email(self, email): + user = self._db.get_user_by_email(email) + if user is None: + return None + + return { + 'id': user.id, + 'email': user.email, + 'user_type': user.user_type, + 'banned_date': user.banned_date + } + + def ban_user(self, email): + user = self._db.get_user_by_email(email) + if user is None: + raise InvalidUserError() + if user.banned_date is not None: + raise UserAlreadyBannedError() + + self._db.ban_user(user) + + return { + 'id': user.id, + 'email': user.email, + 'user_type': user.user_type, + 'banned_date': user.banned_date + } + #################################### # Train Job #################################### @@ -580,8 +600,8 @@ def get_models_of_task(self, user_id, task): # Private / Users #################################### - def _seed_users(self): - logger.info('Seeding users...') + def _seed_superadmin(self): + logger.info('Seeding superadmin...') # Seed superadmin try: diff --git a/rafiki/admin/app.py b/rafiki/admin/app.py index c9019224..155473e3 100644 --- a/rafiki/admin/app.py +++ b/rafiki/admin/app.py @@ -3,9 +3,10 @@ import os import traceback import json +from datetime import datetime from rafiki.constants import UserType -from rafiki.utils.auth import generate_token, decode_token, auth +from rafiki.utils.auth import generate_token, decode_token, auth, UnauthorizedError from .admin import Admin @@ -20,27 +21,49 @@ def index(): # Users #################################### -@app.route('/user', methods=['POST']) +@app.route('/users', methods=['POST']) @auth([UserType.ADMIN]) def create_user(auth): admin = get_admin() params = get_request_params() + # Only superadmins can create admins + if auth['user_type'] != UserType.SUPERADMIN and \ + params['user_type'] in [UserType.ADMIN, UserType.SUPERADMIN]: + raise UnauthorizedError() + with admin: return jsonify(admin.create_user(**params)) -@app.route('/users', methods=['POST']) +@app.route('/users', methods=['GET']) @auth([UserType.ADMIN]) -def create_users(auth): +def get_users(auth): admin = get_admin() params = get_request_params() - # Expect csv file as bytes - csv_file_bytes = request.files['csv_file_bytes'].read() - params['csv_file_bytes'] = csv_file_bytes + with admin: + return jsonify(admin.get_users(**params)) + +@app.route('/users', methods=['DELETE']) +@auth([UserType.ADMIN]) +def ban_user(auth): + admin = get_admin() + params = get_request_params() with admin: - return jsonify(admin.create_users(**params)) + user = admin.get_user_by_email(params['email']) + + if user is not None: + # Only superadmins can ban admins + if auth['user_type'] != UserType.SUPERADMIN and \ + user['user_type'] in [UserType.ADMIN, UserType.SUPERADMIN]: + raise UnauthorizedError() + + # Cannot ban yourself + if auth['user_id'] == user['id']: + raise UnauthorizedError() + + return jsonify(admin.ban_user(**params)) @app.route('/tokens', methods=['POST']) def generate_user_token(): @@ -51,12 +74,11 @@ def generate_user_token(): with admin: user = admin.authenticate_user(**params) - auth = { - 'user_id': user['id'], - 'user_type': user['user_type'] - } + # User cannot be banned + if user.get('banned_date') is not None and datetime.now() > user.get('banned_date'): + raise UnauthorizedError('User is banned') - token = generate_token(auth) + token = generate_token(user) return jsonify({ 'user_id': user['id'], diff --git a/rafiki/advisor/app.py b/rafiki/advisor/app.py index 631e3401..a513f8a5 100644 --- a/rafiki/advisor/app.py +++ b/rafiki/advisor/app.py @@ -18,26 +18,6 @@ def index(): return 'Rafiki Advisor is up.' -@app.route('/tokens', methods=['POST']) -def generate_user_token(): - params = get_request_params() - - # Only superadmin can authenticate (other users must use Rafiki Admin) - if not (params['email'] == SUPERADMIN_EMAIL and \ - params['password'] == SUPERADMIN_PASSWORD): - raise UnauthorizedError() - - auth = { - 'user_type': UserType.SUPERADMIN - } - - token = generate_token(auth) - - return jsonify({ - 'user_type': auth['user_type'], - 'token': token - }) - @app.route('/advisors', methods=['POST']) @auth([UserType.ADMIN, UserType.APP_DEVELOPER]) def create_advisor(auth): diff --git a/rafiki/client/client.py b/rafiki/client/client.py index 155d2c43..58687a3c 100644 --- a/rafiki/client/client.py +++ b/rafiki/client/client.py @@ -36,6 +36,8 @@ def login(self, email, password): App developers can create, list and stop train and inference jobs, as well as list models. Model developers can create and list models. + The login session (the session token) expires in 1 hour. + :param str email: User's email :param str password: User's password @@ -80,7 +82,8 @@ def create_user(self, email, password, user_type): ''' Creates a Rafiki user. - Only admins can manage users. + Only admins can create users (except for admins). + Only superadmins can create admins. :param str email: The new user's email :param str password: The new user's password @@ -90,33 +93,41 @@ def create_user(self, email, password, user_type): :returns: Created user as dictionary :rtype: dict[str, any] ''' - data = self._post('/user', json={ + data = self._post('/users', json={ 'email': email, 'password': password, 'user_type': user_type }) return data - def create_users(self, csv_file_path): + def get_users(self): ''' - Creates multiple Rafiki users. - - :param str csv_file_path: Path to a single csv file containing users to seed + Lists all Rafiki users. + + Only admins can list all users. - :returns: Created users as list of dictionaries + :returns: List of users :rtype: dict[str, any][] ''' + data = self._get('/users') + return data - f = open(csv_file_path, 'rb') - csv_file_bytes = f.read() - f.close() + def ban_user(self, email): + ''' + Bans a Rafiki user, disallowing logins. + + This action is irrevisible. + Only admins can ban users (except for admins). + Only superadmins can ban admins. - data = self._post( - '/users', - files={ - 'csv_file_bytes': csv_file_bytes - } - ) + :param str email: The user's email + + :returns: Banned user as dictionary + :rtype: dict[str, any] + ''' + data = self._delete('/users', json={ + 'email': email + }) return data #################################### diff --git a/rafiki/constants.py b/rafiki/constants.py index 7e6665b5..8ff3a0de 100644 --- a/rafiki/constants.py +++ b/rafiki/constants.py @@ -48,7 +48,6 @@ class UserType(): ADMIN = 'ADMIN' MODEL_DEVELOPER = 'MODEL_DEVELOPER' APP_DEVELOPER = 'APP_DEVELOPER' - USER = 'USER' class AdvisorType(): BTB_GP = 'BTB_GP' diff --git a/rafiki/db/database.py b/rafiki/db/database.py index d9378fb8..2d1a3b6a 100644 --- a/rafiki/db/database.py +++ b/rafiki/db/database.py @@ -1,15 +1,17 @@ -import datetime +from datetime import datetime import os from sqlalchemy import create_engine, distinct from sqlalchemy.orm import sessionmaker -from rafiki.constants import TrainJobStatus, \ +from rafiki.constants import TrainJobStatus, UserType, \ TrialStatus, ServiceStatus, InferenceJobStatus, ModelAccessRight from .schema import Base, TrainJob, SubTrainJob, TrainJobWorker, \ InferenceJob, Trial, Model, User, Service, InferenceJobWorker, \ TrialLog +class InvalidUserTypeError(Exception): pass + class Database(object): def __init__(self, host=os.environ.get('POSTGRES_HOST', 'localhost'), @@ -36,6 +38,7 @@ def __init__(self, #################################### def create_user(self, email, password_hash, user_type): + self._validate_user_type(user_type) user = User( email=email, password_hash=password_hash, @@ -44,10 +47,22 @@ def create_user(self, email, password_hash, user_type): self._session.add(user) return user + def ban_user(self, user): + user.banned_date = datetime.utcnow() + self._session.add(user) + def get_user_by_email(self, email): user = self._session.query(User).filter(User.email == email).first() return user + def get_users(self): + users = self._session.query(User).all() + return users + + def _validate_user_type(self, user_type): + if user_type not in [UserType.SUPERADMIN, UserType.ADMIN, UserType.APP_DEVELOPER, UserType.MODEL_DEVELOPER]: + raise InvalidUserTypeError() + #################################### # Train Jobs #################################### @@ -135,7 +150,7 @@ def mark_sub_train_job_as_running(self, sub_train_job): def mark_sub_train_job_as_stopped(self, sub_train_job): sub_train_job.status = TrainJobStatus.STOPPED - sub_train_job.datetime_stopped = datetime.datetime.utcnow() + sub_train_job.datetime_stopped = datetime.utcnow() self._session.add(sub_train_job) return sub_train_job @@ -206,13 +221,13 @@ def mark_inference_job_as_running(self, inference_job): def mark_inference_job_as_stopped(self, inference_job): inference_job.status = InferenceJobStatus.STOPPED - inference_job.datetime_stopped = datetime.datetime.utcnow() + inference_job.datetime_stopped = datetime.utcnow() self._session.add(inference_job) return inference_job def mark_inference_job_as_errored(self, inference_job): inference_job.status = InferenceJobStatus.ERRORED - inference_job.datetime_stopped = datetime.datetime.utcnow() + inference_job.datetime_stopped = datetime.utcnow() self._session.add(inference_job) return inference_job @@ -287,12 +302,12 @@ def mark_service_as_running(self, service): def mark_service_as_errored(self, service): service.status = ServiceStatus.ERRORED - service.datetime_stopped = datetime.datetime.utcnow() + service.datetime_stopped = datetime.utcnow() self._session.add(service) def mark_service_as_stopped(self, service): service.status = ServiceStatus.STOPPED - service.datetime_stopped = datetime.datetime.utcnow() + service.datetime_stopped = datetime.utcnow() self._session.add(service) def get_service(self, service_id): @@ -418,14 +433,14 @@ def mark_trial_as_running(self, trial, knobs): def mark_trial_as_errored(self, trial): trial.status = TrialStatus.ERRORED - trial.datetime_stopped = datetime.datetime.utcnow() + trial.datetime_stopped = datetime.utcnow() self._session.add(trial) return trial def mark_trial_as_complete(self, trial, score, parameters): trial.status = TrialStatus.COMPLETED trial.score = score - trial.datetime_stopped = datetime.datetime.utcnow() + trial.datetime_stopped = datetime.utcnow() trial.parameters = parameters self._session.add(trial) return trial @@ -437,7 +452,7 @@ def add_trial_log(self, trial, line, level): def mark_trial_as_terminated(self, trial): trial.status = TrialStatus.TERMINATED - trial.datetime_stopped = datetime.datetime.utcnow() + trial.datetime_stopped = datetime.utcnow() self._session.add(trial) return trial diff --git a/rafiki/db/schema.py b/rafiki/db/schema.py index 337a80c1..d24b3067 100644 --- a/rafiki/db/schema.py +++ b/rafiki/db/schema.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, String, Float, ForeignKey, Integer, Binary, DateTime from sqlalchemy.dialects.postgresql import JSON, ARRAY import uuid -import datetime +from datetime import datetime from rafiki.constants import InferenceJobStatus, ServiceStatus, TrainJobStatus, \ TrialStatus, ModelAccessRight @@ -13,7 +13,7 @@ def generate_uuid(): return str(uuid.uuid4()) def generate_datetime(): - return datetime.datetime.utcnow() + return datetime.utcnow() class InferenceJob(Base): __tablename__ = 'inference_job' @@ -125,4 +125,5 @@ class User(Base): email = Column(String, unique=True, nullable=False) password_hash = Column(Binary, nullable=False) user_type = Column(String, nullable=False) + banned_date = Column(DateTime, default=None) \ No newline at end of file diff --git a/rafiki/model/log.py b/rafiki/model/log.py index 4b2e9045..1cbcfbca 100644 --- a/rafiki/model/log.py +++ b/rafiki/model/log.py @@ -1,6 +1,6 @@ import os import traceback -import datetime +from datetime import datetime import json import logging @@ -106,7 +106,7 @@ def set_logger(self, logger): def _log(self, log_type, log_dict={}): log_dict['type'] = log_type - log_dict['time'] = datetime.datetime.now().strftime(MODEL_LOG_DATETIME_FORMAT) + log_dict['time'] = datetime.now().strftime(MODEL_LOG_DATETIME_FORMAT) log_line = json.dumps(log_dict) self._logger.info(log_line) diff --git a/rafiki/utils/auth.py b/rafiki/utils/auth.py index 7729728e..f49f739c 100644 --- a/rafiki/utils/auth.py +++ b/rafiki/utils/auth.py @@ -2,14 +2,22 @@ import os import jwt from functools import wraps +from datetime import datetime, timedelta from rafiki.constants import UserType from rafiki.config import APP_SECRET +TOKEN_EXPIRATION_HOURS = 1 + class UnauthorizedError(Exception): pass class InvalidAuthorizationHeaderError(Exception): pass -def generate_token(payload): +def generate_token(user): + payload = { + 'user_id': user['id'], + 'user_type': user['user_type'], + 'exp': datetime.utcnow() + timedelta(hours=TOKEN_EXPIRATION_HOURS) + } token = jwt.encode(payload, APP_SECRET, algorithm='HS256') return token.decode('utf-8') @@ -18,7 +26,7 @@ def decode_token(token): return payload def auth(user_types=[]): - # Superadmin can do anything + # Superadmins can do anything user_types.append(UserType.SUPERADMIN) def decorator(f): diff --git a/scripts/build_docs.sh b/scripts/build_docs.sh index 907c4bfe..5edd0047 100644 --- a/scripts/build_docs.sh +++ b/scripts/build_docs.sh @@ -6,4 +6,6 @@ else fi pip install sphinx sphinx_rtd_theme -sphinx-build -b html . docs/$DOCS_DIR/ \ No newline at end of file +sphinx-build -b html docs/ docs/$DOCS_DIR/ + +echo "Generated documentation site at ./docs/$DOCS_DIR/index.html" \ No newline at end of file diff --git a/scripts/save_db.sh b/scripts/save_db.sh index 2708b455..115fb929 100644 --- a/scripts/save_db.sh +++ b/scripts/save_db.sh @@ -7,8 +7,9 @@ then if [ $ok = "n" ] then echo "Not dumping database!" - else - echo "Dumping database to $DUMP_FILE..." - docker exec $POSTGRES_HOST pg_dump -U postgres --if-exists --clean $POSTGRES_DB > $DUMP_FILE + exit 0 fi fi + +echo "Dumping database to $DUMP_FILE..." +docker exec $POSTGRES_HOST pg_dump -U postgres --if-exists --clean $POSTGRES_DB > $DUMP_FILE diff --git a/scripts/utils.sh b/scripts/utils.sh index 201f3795..fcd4c729 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -20,7 +20,7 @@ ensure_stable() echo "$1 is running" else echo "Error running $1" - echo "Maybe $1 hasn't previously been stopped - try running `scripts/stop.sh`?" + echo "Maybe $1 hasn't previously been stopped - try running scripts/stop.sh?" if ! [ -z "$LOG_FILE_PATH" ] then echo "Check the logs at $LOG_FILE_PATH"