diff --git a/app.py b/app.py
index a8b701c66..b2f41cd69 100644
--- a/app.py
+++ b/app.py
@@ -16,6 +16,7 @@ from data.users import UserAuthentication
from util.analytics import Analytics
from util.exceptionlog import Sentry
from util.queuemetrics import QueueMetrics
+from util.expiration import Expiration
from data.billing import Billing
from data.buildlogs import BuildLogs
from data.queue import WorkQueue
@@ -64,6 +65,7 @@ sentry = Sentry(app)
build_logs = BuildLogs(app)
queue_metrics = QueueMetrics(app)
authentication = UserAuthentication(app)
+expiration = Expiration(app)
tf = app.config['DB_TRANSACTION_FACTORY']
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'], tf)
diff --git a/data/database.py b/data/database.py
index 71f88fb91..ffa8c909e 100644
--- a/data/database.py
+++ b/data/database.py
@@ -117,7 +117,7 @@ class FederatedLogin(BaseModel):
class Visibility(BaseModel):
- name = CharField(index=True)
+ name = CharField(index=True, unique=True)
class Repository(BaseModel):
@@ -136,7 +136,7 @@ class Repository(BaseModel):
class Role(BaseModel):
- name = CharField(index=True)
+ name = CharField(index=True, unique=True)
class RepositoryPermission(BaseModel):
@@ -189,7 +189,7 @@ class AccessToken(BaseModel):
class BuildTriggerService(BaseModel):
- name = CharField(index=True)
+ name = CharField(index=True, unique=True)
class RepositoryBuildTrigger(BaseModel):
@@ -283,7 +283,7 @@ class QueueItem(BaseModel):
class LogEntryKind(BaseModel):
- name = CharField(index=True)
+ name = CharField(index=True, unique=True)
class LogEntry(BaseModel):
@@ -330,7 +330,7 @@ class OAuthAccessToken(BaseModel):
class NotificationKind(BaseModel):
- name = CharField(index=True)
+ name = CharField(index=True, unique=True)
class Notification(BaseModel):
diff --git a/data/migrations/versions/5a07499ce53f_set_up_initial_database.py b/data/migrations/versions/5a07499ce53f_set_up_initial_database.py
index 3c4a8de5d..23aaf506a 100644
--- a/data/migrations/versions/5a07499ce53f_set_up_initial_database.py
+++ b/data/migrations/versions/5a07499ce53f_set_up_initial_database.py
@@ -140,6 +140,7 @@ def upgrade():
[
{'id':1, 'name':'password_required'},
{'id':2, 'name':'over_private_usage'},
+ {'id':3, 'name':'expiring_license'},
])
op.create_table('teamrole',
diff --git a/data/model/legacy.py b/data/model/legacy.py
index 76d0123be..f27e29170 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -1622,14 +1622,20 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid,
.where(RepositoryBuildTrigger.uuid == trigger_uuid))
-def create_notification(kind, target, metadata={}):
- kind_ref = NotificationKind.get(name=kind)
+def create_notification(kind_name, target, metadata={}):
+ kind_ref = NotificationKind.get(name=kind_name)
notification = Notification.create(kind=kind_ref, target=target,
metadata_json=json.dumps(metadata))
return notification
-def list_notifications(user, kind=None):
+def create_unique_notification(kind_name, target, metadata={}):
+ with config.app_config['DB_TRANSACTION_FACTORY'](db):
+ if list_notifications(target, kind_name).count() == 0:
+ create_notification(kind_name, target, metadata)
+
+
+def list_notifications(user, kind_name=None):
Org = User.alias()
AdminTeam = Team.alias()
AdminTeamMember = TeamMember.alias()
@@ -1647,20 +1653,30 @@ def list_notifications(user, kind=None):
.join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id ==
AdminTeamMember.team))
.join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user ==
- AdminUser.id)))
+ AdminUser.id))
+ .where((Notification.target == user) |
+ ((AdminUser.id == user) & (TeamRole.name == 'admin')))
+ .order_by(Notification.created)
+ .desc())
- where_clause = ((Notification.target == user) |
- ((AdminUser.id == user) &
- (TeamRole.name == 'admin')))
-
- if kind:
- where_clause = where_clause & (NotificationKind.name == kind)
+ if kind_name:
+ query = (query
+ .switch(Notification)
+ .join(NotificationKind)
+ .where(NotificationKind.name == kind_name))
- return query.where(where_clause).order_by(Notification.created).desc()
+ return query
-def delete_notifications_by_kind(target, kind):
- kind_ref = NotificationKind.get(name=kind)
+def delete_all_notifications_by_kind(kind_name):
+ kind_ref = NotificationKind.get(name=kind_name)
+ (Notification.delete()
+ .where(Notification.kind == kind_ref)
+ .execute())
+
+
+def delete_notifications_by_kind(target, kind_name):
+ kind_ref = NotificationKind.get(name=kind_name)
Notification.delete().where(Notification.target == target,
Notification.kind == kind_ref).execute()
diff --git a/initdb.py b/initdb.py
index d3b1daf09..33d2f048e 100644
--- a/initdb.py
+++ b/initdb.py
@@ -233,6 +233,7 @@ def initialize_database():
NotificationKind.create(name='password_required')
NotificationKind.create(name='over_private_usage')
+ NotificationKind.create(name='expiring_license')
NotificationKind.create(name='test_notification')
diff --git a/static/js/app.js b/static/js/app.js
index 75220520c..e41c06a74 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -913,6 +913,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return '/user';
}
}
+ },
+ 'expiring_license': {
+ 'level': 'error',
+ 'message': 'Your license will expire at: {expires_at} ' +
+ '
Please contact Quay.io support to purchase a new license.',
+ 'page': '/contact/'
}
};
diff --git a/test/data/test.db b/test/data/test.db
index 8b3830464..801d3a28a 100644
Binary files a/test/data/test.db and b/test/data/test.db differ
diff --git a/util/expiration.py b/util/expiration.py
new file mode 100644
index 000000000..b3f5732fe
--- /dev/null
+++ b/util/expiration.py
@@ -0,0 +1,76 @@
+import calendar
+import sys
+
+from email.utils import formatdate
+from apscheduler.scheduler import Scheduler
+from datetime import datetime, timedelta
+
+from data import model
+
+
+class ExpirationScheduler(object):
+ def __init__(self, utc_create_notifications_date, utc_terminate_processes_date):
+ self._scheduler = Scheduler()
+ self._termination_date = utc_terminate_processes_date
+
+ soon = datetime.now() + timedelta(seconds=1)
+
+ if utc_create_notifications_date > datetime.utcnow():
+ self._scheduler.add_date_job(model.delete_all_notifications_by_kind, soon,
+ ['expiring_license'])
+
+ local_notifications_date = self._utc_to_local(utc_create_notifications_date)
+ self._scheduler.add_date_job(self._generate_notifications, local_notifications_date)
+ else:
+ self._scheduler.add_date_job(self._generate_notifications, soon)
+
+ local_termination_date = self._utc_to_local(utc_terminate_processes_date)
+ self._scheduler.add_date_job(self._terminate, local_termination_date)
+
+ @staticmethod
+ def _format_date(date):
+ """ Output an RFC822 date format. """
+ if date is None:
+ return None
+ return formatdate(calendar.timegm(date.utctimetuple()))
+
+ @staticmethod
+ def _utc_to_local(utc_dt):
+ # get integer timestamp to avoid precision lost
+ timestamp = calendar.timegm(utc_dt.timetuple())
+ local_dt = datetime.fromtimestamp(timestamp)
+ return local_dt.replace(microsecond=utc_dt.microsecond)
+
+ def _generate_notifications(self):
+ for user in model.get_active_users():
+ model.create_unique_notification('expiring_license', user,
+ {'expires_at': self._format_date(self._termination_date)})
+
+ @staticmethod
+ def _terminate():
+ sys.exit(1)
+
+ def start(self):
+ self._scheduler.start()
+
+
+class Expiration(object):
+ def __init__(self, app=None):
+ self.app = app
+ if app is not None:
+ self.state = self.init_app(app)
+ else:
+ self.state = None
+
+ def init_app(self, app):
+ expiration = ExpirationScheduler(app.config['LICENSE_EXPIRATION_WARNING'],
+ app.config['LICENSE_EXPIRATION'])
+ expiration.start()
+
+ # register extension with app
+ app.extensions = getattr(app, 'extensions', {})
+ app.extensions['expiration'] = expiration
+ return expiration
+
+ def __getattr__(self, name):
+ return getattr(self.state, name, None)