Fix data_seed migration data being skipped (#22860)

* Skip initial migration if seeding on initialize.py

* Remove redundant code

* Replace flush db with reset_db to ensure --clean resets migrations

* Add django check for db charset

* Remove uneeded import_licenses command

* Fix tests
This commit is contained in:
Kevin Meinhardt 2024-11-22 18:58:01 +01:00 коммит произвёл GitHub
Родитель 21429e9710
Коммит a2945aa232
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 69 добавлений и 77 удалений

Просмотреть файл

@ -1,6 +1,3 @@
import os
import shutil
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
@ -13,56 +10,25 @@ class Command(BaseDataCommand):
'generated add-ons, and data from AMO production.' 'generated add-ons, and data from AMO production.'
) )
def _clean_storage(self, root: str, dir_dict: dict[str, str | dict]) -> None:
for key, value in dir_dict.items():
curr_path = os.path.join(root, key)
if isinstance(value, dict):
self._clean_storage(curr_path, value)
else:
shutil.rmtree(curr_path, ignore_errors=True)
os.makedirs(curr_path, exist_ok=True)
def clean_storage(self):
self.logger.info('Cleaning storage...')
self._clean_storage(
settings.STORAGE_ROOT,
{
'files': '',
'shared_storage': {
'tmp': {
'addons': '',
'data': '',
'file_viewer': '',
'guarded-addons': '',
'icon': '',
'log': '',
'persona_header': '',
'preview': '',
'test': '',
'uploads': '',
},
'uploads': '',
},
},
)
def handle(self, *args, **options): def handle(self, *args, **options):
num_addons = 10 num_addons = 10
num_themes = 5 num_themes = 5
# Delete any existing data_seed backup
self.clean_dir(self.data_backup_init) self.clean_dir(self.data_backup_init)
self.logger.info('Resetting database...') self.logger.info('Resetting database...')
call_command('flush', '--noinput') call_command('reset_db', '--no-utf8', '--noinput')
# Delete any local storage files
# This should happen after we reset the database to ensure any records
# relying on storage are deleted.
self.clean_storage() self.clean_storage()
# reindex --wipe will force the ES mapping to be re-installed. # Migrate the database
call_command('reindex', '--wipe', '--force', '--noinput')
call_command('migrate', '--noinput') call_command('migrate', '--noinput')
self.logger.info('Loading initial data...') self.logger.info('Loading initial data...')
call_command('loaddata', 'initial.json') call_command('loaddata', 'initial.json')
call_command('import_prod_versions') call_command('import_prod_versions')
call_command('import_licenses')
call_command( call_command(
'createsuperuser', 'createsuperuser',
'--no-input', '--no-input',

Просмотреть файл

@ -33,6 +33,7 @@ class Command(BaseDataCommand):
""" """
Create the database. Create the database.
""" """
logging.info(f'options: {options}')
# We need to support skipping loading/seeding when desired. # We need to support skipping loading/seeding when desired.
# Like in CI environments where you don't want to load data every time. # Like in CI environments where you don't want to load data every time.
if settings.DATA_BACKUP_SKIP: if settings.DATA_BACKUP_SKIP:
@ -41,10 +42,12 @@ class Command(BaseDataCommand):
) )
return return
clean = options.get('clean') # If DB empty or we are explicitly cleaning, then bail with data_seed.
load = options.get('load') if options.get('clean') or not self.local_admin_exists():
logging.info(f'options: {options}') call_command('data_seed')
return
load = options.get('load')
# We always migrate the DB. # We always migrate the DB.
logging.info('Migrating...') logging.info('Migrating...')
call_command('migrate', '--noinput') call_command('migrate', '--noinput')
@ -52,9 +55,6 @@ class Command(BaseDataCommand):
# If we specify a specifi backup, simply load that. # If we specify a specifi backup, simply load that.
if load: if load:
call_command('data_load', '--name', load) call_command('data_load', '--name', load)
# If DB empty or we are explicitly cleaning, then reseed.
elif clean or not self.local_admin_exists():
call_command('data_seed')
# We should reindex even if no data is loaded/modified # We should reindex even if no data is loaded/modified
# because we might have a fresh instance of elasticsearch # because we might have a fresh instance of elasticsearch
else: else:

Просмотреть файл

@ -152,15 +152,13 @@ def test_generate_jsi18n_files():
class BaseTestDataCommand(TestCase): class BaseTestDataCommand(TestCase):
class Commands: class Commands:
flush = mock.call('flush', '--noinput') reset_db = mock.call('reset_db', '--no-utf8', '--noinput')
migrate = mock.call('migrate', '--noinput') migrate = mock.call('migrate', '--noinput')
data_seed = mock.call('data_seed') data_seed = mock.call('data_seed')
flush = mock.call('flush', '--noinput')
reindex = mock.call('reindex', '--wipe', '--force', '--noinput') reindex = mock.call('reindex', '--wipe', '--force', '--noinput')
load_initial_data = mock.call('loaddata', 'initial.json') load_initial_data = mock.call('loaddata', 'initial.json')
import_prod_versions = mock.call('import_prod_versions') import_prod_versions = mock.call('import_prod_versions')
import_licenses = mock.call('import_licenses')
createsuperuser = mock.call( createsuperuser = mock.call(
'createsuperuser', 'createsuperuser',
'--no-input', '--no-input',
@ -278,16 +276,15 @@ class TestInitializeDataCommand(BaseTestDataCommand):
def test_handle_with_clean_and_load_arguments(self): def test_handle_with_clean_and_load_arguments(self):
""" """
Test running the 'initialize' command with both '--clean' and '--load' Test running the 'initialize' command with both '--clean' and '--load'
arguments. Expected: Command should prioritize '--load' and perform arguments. Expected: Command should prioritize '--clean' and perform
migration, loading. migration and seeding.
""" """
name = 'test' name = 'test'
call_command('initialize', clean=True, load=name) call_command('initialize', clean=True, load=name)
self._assert_commands_called_in_order( self._assert_commands_called_in_order(
self.mocks['mock_call_command'], self.mocks['mock_call_command'],
[ [
self.mock_commands.migrate, self.mock_commands.data_seed,
self.mock_commands.data_load(name),
], ],
) )
@ -301,7 +298,6 @@ class TestInitializeDataCommand(BaseTestDataCommand):
self._assert_commands_called_in_order( self._assert_commands_called_in_order(
self.mocks['mock_call_command'], self.mocks['mock_call_command'],
[ [
self.mock_commands.migrate,
self.mock_commands.data_seed, self.mock_commands.data_seed,
], ],
) )
@ -332,7 +328,6 @@ class TestInitializeDataCommand(BaseTestDataCommand):
self._assert_commands_called_in_order( self._assert_commands_called_in_order(
self.mocks['mock_call_command'], self.mocks['mock_call_command'],
[ [
self.mock_commands.migrate,
self.mock_commands.data_seed, self.mock_commands.data_seed,
], ],
) )
@ -343,6 +338,7 @@ class TestInitializeDataCommand(BaseTestDataCommand):
Expected: The command exits with an error and does not proceed to seeding Expected: The command exits with an error and does not proceed to seeding
or loading data. or loading data.
""" """
self.with_local_admin()
self.mocks['mock_call_command'].side_effect = Exception('test') self.mocks['mock_call_command'].side_effect = Exception('test')
with pytest.raises(Exception) as context: with pytest.raises(Exception) as context:
call_command('initialize') call_command('initialize')
@ -599,6 +595,10 @@ class TestSeedDataCommand(BaseTestDataCommand):
'mock_clean_dir', 'mock_clean_dir',
'olympia.amo.management.commands.data_seed.BaseDataCommand.clean_dir', 'olympia.amo.management.commands.data_seed.BaseDataCommand.clean_dir',
), ),
(
'mock_clean_storage',
'olympia.amo.management.commands.data_seed.BaseDataCommand.clean_storage',
),
) )
self.mocks = {} self.mocks = {}
@ -611,15 +611,18 @@ class TestSeedDataCommand(BaseTestDataCommand):
def test_default(self): def test_default(self):
call_command('data_seed') call_command('data_seed')
self.mocks['mock_clean_dir'].assert_called_once_with(
self.base_data_command.data_backup_init
)
self.mocks['mock_clean_storage'].assert_called_once()
self._assert_commands_called_in_order( self._assert_commands_called_in_order(
self.mocks['mock_call_command'], self.mocks['mock_call_command'],
[ [
self.mock_commands.flush, self.mock_commands.reset_db,
self.mock_commands.reindex,
self.mock_commands.migrate, self.mock_commands.migrate,
self.mock_commands.load_initial_data, self.mock_commands.load_initial_data,
self.mock_commands.import_prod_versions, self.mock_commands.import_prod_versions,
self.mock_commands.import_licenses,
self.mock_commands.createsuperuser, self.mock_commands.createsuperuser,
self.mock_commands.load_zadmin_users, self.mock_commands.load_zadmin_users,
self.mock_commands.generate_addons('firefox', 10), self.mock_commands.generate_addons('firefox', 10),

Просмотреть файл

@ -1,15 +0,0 @@
from django.core.management.base import BaseCommand
from olympia.constants.licenses import ALL_LICENSES
from olympia.versions.models import License
class Command(BaseCommand):
help = """Import a the licenses."""
def handle(self, *args, **options):
for license in ALL_LICENSES:
try:
License.objects.get_or_create(builtin=license.builtin)
except Exception:
continue

Просмотреть файл

@ -9,6 +9,7 @@ from django.conf import settings
from django.core.checks import Error, Tags, register from django.core.checks import Error, Tags, register
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.db import connection
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from olympia.core.utils import get_version_json from olympia.core.utils import get_version_json
@ -98,6 +99,26 @@ def static_check(app_configs, **kwargs):
return errors return errors
@register(CustomTags.custom_setup)
def db_charset_check(app_configs, **kwargs):
errors = []
with connection.cursor() as cursor:
cursor.execute("SHOW VARIABLES LIKE 'character_set_database';")
result = cursor.fetchone()
if result[1] != settings.DB_CHARSET:
errors.append(
Error(
'Database charset invalid. '
f'Expected {settings.DB_CHARSET}, '
f'recieved {result[1]}',
id='setup.E005',
)
)
return errors
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
name = 'olympia.core' name = 'olympia.core'
verbose_name = _('Core') verbose_name = _('Core')

Просмотреть файл

@ -2,10 +2,22 @@ from unittest import mock
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import SystemCheckError from django.core.management.base import SystemCheckError
from django.test import SimpleTestCase from django.test import TestCase
class SystemCheckIntegrationTest(SimpleTestCase): class SystemCheckIntegrationTest(TestCase):
@mock.patch('olympia.core.apps.connection.cursor')
def test_db_charset_check(self, mock_cursor):
mock_cursor.return_value.__enter__.return_value.fetchone.return_value = (
'character_set_database',
'utf8mb3',
)
with self.assertRaisesMessage(
SystemCheckError,
'Database charset invalid. Expected utf8mb4, recieved utf8mb3',
):
call_command('check')
def test_uwsgi_check(self): def test_uwsgi_check(self):
call_command('check') call_command('check')

Просмотреть файл

@ -130,6 +130,9 @@ CORS_ALLOW_HEADERS = list(default_headers) + [
'x-country-code', 'x-country-code',
] ]
DB_ENGINE = 'olympia.core.db.mysql'
DB_CHARSET = 'utf8mb4'
def get_db_config(environ_var, atomic_requests=True): def get_db_config(environ_var, atomic_requests=True):
values = env.db(var=environ_var, default='mysql://root:@127.0.0.1/olympia') values = env.db(var=environ_var, default='mysql://root:@127.0.0.1/olympia')
@ -142,9 +145,9 @@ def get_db_config(environ_var, atomic_requests=True):
'ATOMIC_REQUESTS': atomic_requests, 'ATOMIC_REQUESTS': atomic_requests,
# Pool our database connections up for 300 seconds # Pool our database connections up for 300 seconds
'CONN_MAX_AGE': 300, 'CONN_MAX_AGE': 300,
'ENGINE': 'olympia.core.db.mysql', 'ENGINE': DB_ENGINE,
'OPTIONS': { 'OPTIONS': {
'charset': 'utf8mb4', 'charset': DB_CHARSET,
'sql_mode': 'STRICT_ALL_TABLES', 'sql_mode': 'STRICT_ALL_TABLES',
'isolation_level': 'read committed', 'isolation_level': 'read committed',
}, },
@ -167,6 +170,8 @@ REPLICA_DATABASES = []
LOCAL_ADMIN_EMAIL = 'local_admin@mozilla.com' LOCAL_ADMIN_EMAIL = 'local_admin@mozilla.com'
LOCAL_ADMIN_USERNAME = 'local_admin' LOCAL_ADMIN_USERNAME = 'local_admin'
DJANGO_EXTENSIONS_RESET_DB_MYSQL_ENGINES = [DB_ENGINE]
# Local time zone for this installation. Choices can be found here: # Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems. # although not all choices may be available on all operating systems.