Integrate a migration-detector middleware

This attempts to detect if there are migrations in-progress and will
force display an interstitial page in the process that attempts to
load the index page every 10s until it succeeds.

This is only attached in production settings so the development
environment can proceed even if the migrations haven't been applied yet
This commit is contained in:
Matthew Jones
2017-09-11 11:09:45 -04:00
parent 0e04b8e4d4
commit b39db745d4
8 changed files with 74 additions and 3 deletions

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ awx/ui/static
awx/ui/build_test awx/ui/build_test
awx/ui/client/languages awx/ui/client/languages
awx/ui/templates/ui/index.html awx/ui/templates/ui/index.html
awx/ui/templates/ui/installing.html
# Tower setup playbook testing # Tower setup playbook testing
setup/test/roles/postgresql setup/test/roles/postgresql

View File

@@ -9,11 +9,13 @@ import six
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.db import IntegrityError from django.db.migrations.executor import MigrationExecutor
from django.db import IntegrityError, connection
from django.utils.functional import curry from django.utils.functional import curry
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, redirect
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from awx.main.models import ActivityStream from awx.main.models import ActivityStream
from awx.api.authentication import TokenAuthentication from awx.api.authentication import TokenAuthentication
@@ -162,3 +164,12 @@ class URLModificationMiddleware(object):
if request.path_info != new_path: if request.path_info != new_path:
request.path = request.path.replace(request.path_info, new_path) request.path = request.path.replace(request.path_info, new_path)
request.path_info = new_path request.path_info = new_path
class MigrationRanCheckMiddleware(object):
def process_request(self, request):
executor = MigrationExecutor(connection)
plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
if bool(plan) and 'migrations_notran' not in request.path:
return redirect(reverse("ui:migrations_notran"))

View File

@@ -87,6 +87,8 @@ settings_files = os.path.join(settings_dir, '*.py')
settings_file = os.environ.get('AWX_SETTINGS_FILE', settings_file = os.environ.get('AWX_SETTINGS_FILE',
'/etc/tower/settings.py') '/etc/tower/settings.py')
MIDDLEWARE_CLASSES = ('awx.main.middleware.MigrationRanCheckMiddleware',) + MIDDLEWARE_CLASSES
# Attempt to load settings from /etc/tower/settings.py first, followed by # Attempt to load settings from /etc/tower/settings.py first, followed by
# /etc/tower/conf.d/*.py. # /etc/tower/conf.d/*.py.
try: try:

View File

@@ -34,7 +34,7 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
if not hasattr(request, 'successful_authenticator'): if not hasattr(request, 'successful_authenticator'):
request.successful_authenticator = None request.successful_authenticator = None
if not request.path.startswith('/sso/'): if not request.path.startswith('/sso/') and 'migrations_notran' not in request.path:
# If token isn't present but we still have a user logged in via Django # If token isn't present but we still have a user logged in via Django
# sessions, log them out. # sessions, log them out.

View File

@@ -25,6 +25,8 @@ const APP_ENTRY = path.join(SOURCE_PATH, 'app.js');
const VENDOR_ENTRY = path.join(SOURCE_PATH, 'vendor.js'); const VENDOR_ENTRY = path.join(SOURCE_PATH, 'vendor.js');
const INDEX_ENTRY = path.join(CLIENT_PATH, 'index.template.ejs'); const INDEX_ENTRY = path.join(CLIENT_PATH, 'index.template.ejs');
const INDEX_OUTPUT = path.join(UI_PATH, 'templates/ui/index.html'); const INDEX_OUTPUT = path.join(UI_PATH, 'templates/ui/index.html');
const INSTALL_RUNNING_ENTRY = path.join(CLIENT_PATH, 'installing.template.ejs')
const INSTALL_RUNNING_OUTPUT = path.join(UI_PATH, 'templates/ui/installing.html');
const THEME_ENTRY = path.join(LIB_PATH, 'theme', 'index.less'); const THEME_ENTRY = path.join(LIB_PATH, 'theme', 'index.less');
const OUTPUT = 'js/[name].[hash].js'; const OUTPUT = 'js/[name].[hash].js';
const CHUNKS = ['vendor', 'app']; const CHUNKS = ['vendor', 'app'];
@@ -168,6 +170,19 @@ let base = {
moduleA.files.sort((fileA, fileB) => fileA.includes('js') ? -1 : 1) moduleA.files.sort((fileA, fileB) => fileA.includes('js') ? -1 : 1)
moduleB.files.sort((fileA, fileB) => fileA.includes('js') ? -1 : 1) moduleB.files.sort((fileA, fileB) => fileA.includes('js') ? -1 : 1)
return moduleA.names[0] === 'vendor' ? -1 : 1
}
}),
new HtmlWebpackPlugin({
alwaysWriteToDisk: true,
template: INSTALL_RUNNING_ENTRY,
filename: INSTALL_RUNNING_OUTPUT,
inject: false,
chunks: CHUNKS,
chunksSortMode: (moduleA, moduleB) => {
moduleA.files.sort((fileA, fileB) => fileA.includes('js') ? -1 : 1)
moduleB.files.sort((fileA, fileB) => fileA.includes('js') ? -1 : 1)
return moduleA.names[0] === 'vendor' ? -1 : 1 return moduleA.names[0] === 'vendor' ? -1 : 1
} }
}) })

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en" ng-app="awApp">
<head>
{% load staticfiles %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="{{ STATIC_URL }}assets/favicon.ico?v={{version}}" />
<title ng-bind="tabTitle"></title>
<% htmlWebpackPlugin.files.css.forEach(file => {%>
<link href="{{ STATIC_URL }}<%= file %>" rel="stylesheet" />
<% }) %>
<% htmlWebpackPlugin.files.js.forEach(file => {%>
<script src="{{ STATIC_URL }}<%= file %>"></script>
<% }) %>
<script>
setInterval(function() {
window.location = '/#/index.html';
}, 10000);
</script>
</head>
<body>
<div class="jumbotron">
<div class="container-fluid" id="content-container">
<div class="span4">
<img style="float:left" src="{% static 'assets/logo-header.svg' %}" width="200"/>
<div class="content-heading"><h1>AWX is Upgrading</h1></div>
<p>
AWX is currently upgrading or installing, this page will refresh when done.
</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -6,5 +6,6 @@ from django.conf.urls import *
urlpatterns = patterns('awx.ui.views', urlpatterns = patterns('awx.ui.views',
url(r'^$', 'index', name='index'), url(r'^$', 'index', name='index'),
url(r'^migrations_notran/$', 'migrations_notran', name='migrations_notran'),
url(r'^portal/$', 'portal_redirect', name='portal_redirect'), url(r'^portal/$', 'portal_redirect', name='portal_redirect'),
) )

View File

@@ -20,3 +20,9 @@ class PortalRedirectView(RedirectView):
url = '/#/portal' url = '/#/portal'
portal_redirect = PortalRedirectView.as_view() portal_redirect = PortalRedirectView.as_view()
class MigrationsNotran(TemplateView):
template_name = 'ui/installing.html'
migrations_notran = MigrationsNotran.as_view()