membermatters / MemberMatters

An open source membership, access and payments portal for makerspaces and community groups.
https://membermatters.org
MIT License
41 stars 23 forks source link

Trouble bootstrapping database in v2.7.0 #152

Closed rechner closed 2 years ago

rechner commented 2 years ago

Hello! Was super pleased to find this project, and am planning to use this at our up-and-coming hackerspace, @pawprintprototyping. I had an instance of the previous release, v2.6.3 running in a docker container with an sqlite database without much fuss, but in trying to upgrade I've run into an error running migrations:

Describe the bug When configured to use a new or existing sqlite database, I see this exception when Django tries to run migrations:

Sqlite startup container logs ``` web_1 | Operations to perform: web_1 | Apply all migrations: access, admin, api_admin_tools, api_general, api_meeting, auth, contenttypes, database, group, memberbucks, profile, sessions web_1 | Running migrations: web_1 | Traceback (most recent call last): web_1 | File "/usr/src/app/memberportal/manage.py", line 15, in web_1 | execute_from_command_line(sys.argv) web_1 | File "/usr/lib/python3.9/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line web_1 | utility.execute() web_1 | File "/usr/lib/python3.9/site-packages/django/core/management/__init__.py", line 413, in execute web_1 | self.fetch_command(subcommand).run_from_argv(self.argv) web_1 | File "/usr/lib/python3.9/site-packages/django/core/management/base.py", line 354, in run_from_argv web_1 | self.execute(*args, **cmd_options) web_1 | File "/usr/lib/python3.9/site-packages/django/core/management/base.py", line 398, in execute web_1 | output = self.handle(*args, **options) web_1 | File "/usr/lib/python3.9/site-packages/django/core/management/base.py", line 89, in wrapped web_1 | res = handle_func(*args, **kwargs) web_1 | File "/usr/lib/python3.9/site-packages/django/core/management/commands/migrate.py", line 244, in handle web_1 | post_migrate_state = executor.migrate( web_1 | File "/usr/lib/python3.9/site-packages/django/db/migrations/executor.py", line 117, in migrate web_1 | state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial) web_1 | File "/usr/lib/python3.9/site-packages/django/db/migrations/executor.py", line 147, in _migrate_all_forwards web_1 | state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial) web_1 | File "/usr/lib/python3.9/site-packages/django/db/migrations/executor.py", line 230, in apply_migration web_1 | migration_recorded = True web_1 | File "/usr/lib/python3.9/site-packages/django/db/backends/sqlite3/schema.py", line 35, in __exit__ web_1 | self.connection.check_constraints() web_1 | File "/usr/lib/python3.9/site-packages/django/db/backends/sqlite3/base.py", line 353, in check_constraints web_1 | raise IntegrityError( web_1 | django.db.utils.IntegrityError: The row in table 'profile_profile_groups' with primary key '1' has an invalid foreign key: profile_profile_groups.group_id contains a value '1' that does not have a corresponding value in group_group.id. web_1 | Sentry is attempting to send 2 pending error messages web_1 | Waiting up to 2 seconds web_1 | Press Ctrl-C to quit web_1 | Applying group.0011_delete_group...INFO: Detected a first time run. Populating the database with defaults. web_1 | /usr/lib/python3.9/site-packages/django/db/models/fields/__init__.py:1416: RuntimeWarning: DateTimeField Profile.digital_id_token_expire received a naive datetime (2022-01-11 09:02:08.512767) while time zone support is active. web_1 | warnings.warn("DateTimeField %s received a naive datetime (%s)" web_1 | Installed 3 object(s) from 1 fixture(s) ```

Looking at the migration in question, my understanding is that the underlying problem comes down to a limitation of sqlite:

The DROP COLUMN command only works if the column is not referenced by any other parts of the schema and is not a PRIMARY KEY and does not have a UNIQUE constraint. docs

So, it will only be possible to run this migration on a new database that hasn't yet been populated by a fixture, and won't work on an existing database.

To Reproduce Steps to reproduce the behavior:

  1. Start the latest version of the container from dockerhub.
  2. See error

Additional context In the course of debugging, I tried setting up a mariadb instance to see if the migrations would work with that using this docker-compose:

docker-compose.yml ```yaml services: web: image: membermatters/membermatters:latest environment: PORTAL_DOMAIN: https://members.example.com PORTAL_ENV: Prodution MMDB_SECRET: '{"username": "root","host": "db","dbname": "members","password" :"members","port": 3306}' ports: - "8000:8000" depends_on: db: condition: service_healthy db: image: mariadb:latest environment: - MARIADB_DATABASE=members - MARIADB_ROOT_PASSWORD=members healthcheck: test: "/usr/bin/mysql --user=root --password=members --execute 'SHOW DATABASES;'" interval: 5s timeout: 1s retries: 5 ```

Start the container and observe the logs:

docker-compose up
Container logs ``` Traceback (most recent call last): File "/usr/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "/usr/lib/python3.9/site-packages/django/db/backends/mysql/base.py", line 73, in execute return self.cursor.execute(query, args) File "/usr/lib/python3.9/site-packages/MySQLdb/cursors.py", line 206, in execute res = self._query(query) File "/usr/lib/python3.9/site-packages/MySQLdb/cursors.py", line 319, in _query db.query(q) File "/usr/lib/python3.9/site-packages/MySQLdb/connections.py", line 259, in query _mysql.connection.query(self, query) MySQLdb._exceptions.ProgrammingError: (1146, "Table 'membermatters.constance_config' doesn't exist") The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/usr/src/app/memberportal/./manage.py", line 15, in execute_from_command_line(sys.argv) File "/usr/lib/python3.9/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line utility.execute() File "/usr/lib/python3.9/site-packages/django/core/management/__init__.py", line 413, in execute self.fetch_command(subcommand).run_from_argv(self.argv) File "/usr/lib/python3.9/site-packages/django/core/management/base.py", line 354, in run_from_argv self.execute(*args, **cmd_options) File "/usr/lib/python3.9/site-packages/django/core/management/base.py", line 398, in execute output = self.handle(*args, **options) File "/usr/lib/python3.9/site-packages/django/core/management/base.py", line 89, in wrapped res = handle_func(*args, **kwargs) File "/usr/lib/python3.9/site-packages/django/core/management/commands/migrate.py", line 75, in handle self.check(databases=[database]) File "/usr/lib/python3.9/site-packages/django/core/management/base.py", line 419, in check all_issues = checks.run_checks( File "/usr/lib/python3.9/site-packages/django/core/checks/registry.py", line 76, in run_checks new_errors = check(app_configs=app_configs, databases=databases) File "/usr/lib/python3.9/site-packages/django/core/checks/urls.py", line 13, in check_url_config return check_resolver(resolver) File "/usr/lib/python3.9/site-packages/django/core/checks/urls.py", line 23, in check_resolver return check_method() File "/usr/lib/python3.9/site-packages/django/urls/resolvers.py", line 412, in check for pattern in self.url_patterns: File "/usr/lib/python3.9/site-packages/django/utils/functional.py", line 48, in __get__ res = instance.__dict__[self.name] = self.func(instance) File "/usr/lib/python3.9/site-packages/django/urls/resolvers.py", line 598, in url_patterns patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module) File "/usr/lib/python3.9/site-packages/django/utils/functional.py", line 48, in __get__ res = instance.__dict__[self.name] = self.func(instance) File "/usr/lib/python3.9/site-packages/django/urls/resolvers.py", line 591, in urlconf_module return import_module(self.urlconf_name) File "/usr/lib/python3.9/importlib/__init__.py", line 127, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "", line 1030, in _gcd_import File "", line 1007, in _find_and_load File "", line 986, in _find_and_load_unlocked File "", line 680, in _load_unlocked File "", line 855, in exec_module File "", line 228, in _call_with_frames_removed File "/usr/src/app/memberportal/membermatters/urls.py", line 44, in path("", include("api_member_bucks.urls")), File "/usr/lib/python3.9/site-packages/django/urls/conf.py", line 34, in include urlconf_module = import_module(urlconf_module) File "/usr/lib/python3.9/importlib/__init__.py", line 127, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "", line 1030, in _gcd_import File "", line 1007, in _find_and_load File "", line 986, in _find_and_load_unlocked File "", line 680, in _load_unlocked File "", line 855, in exec_module File "", line 228, in _call_with_frames_removed File "/usr/src/app/memberportal/api_member_bucks/urls.py", line 2, in from . import views File "/usr/src/app/memberportal/api_member_bucks/views.py", line 11, in stripe.api_key = config.STRIPE_SECRET_KEY File "/usr/lib/python3.9/site-packages/django/utils/functional.py", line 247, in inner return func(self._wrapped, *args) File "/usr/lib/python3.9/site-packages/constance/base.py", line 19, in __getattr__ result = self._backend.get(key) File "/usr/src/app/memberportal/membermatters/constance_backend.py", line 23, in get value = self._model._default_manager.get(key=key).value File "/usr/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/usr/lib/python3.9/site-packages/django/db/models/query.py", line 431, in get num = len(clone) File "/usr/lib/python3.9/site-packages/django/db/models/query.py", line 262, in __len__ self._fetch_all() File "/usr/lib/python3.9/site-packages/django/db/models/query.py", line 1324, in _fetch_all self._result_cache = list(self._iterable_class(self)) File "/usr/lib/python3.9/site-packages/django/db/models/query.py", line 51, in __iter__ results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size) File "/usr/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1175, in execute_sql cursor.execute(sql, params) File "/usr/lib/python3.9/site-packages/django/db/backends/utils.py", line 66, in execute return self._execute_with_wrappers(sql, params, many=False, executor=self._execute) File "/usr/lib/python3.9/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers return executor(sql, params, many, context) File "/usr/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "/usr/lib/python3.9/site-packages/django/db/utils.py", line 90, in __exit__ raise dj_exc_value.with_traceback(traceback) from exc_value File "/usr/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "/usr/lib/python3.9/site-packages/django/db/backends/mysql/base.py", line 73, in execute return self.cursor.execute(query, args) File "/usr/lib/python3.9/site-packages/MySQLdb/cursors.py", line 206, in execute res = self._query(query) File "/usr/lib/python3.9/site-packages/MySQLdb/cursors.py", line 319, in _query db.query(q) File "/usr/lib/python3.9/site-packages/MySQLdb/connections.py", line 259, in query _mysql.connection.query(self, query) django.db.utils.ProgrammingError: (1146, "Table 'membermatters.constance_config' doesn't exist") ```

From what I can surmise, the problem in this configuration relates to the missing constance config, a bootstrapping problem: the migration script imports the models, which imports a view, which references a config key, which can't be resolved because the Constance migrations haven't run yet.

Additionally, I was hoping to be able to downgrade just changing the container tag, but it seems all tags except latest and dev are removed from docker-hub, so I'll have to build.

Anything else I can try to work around this?

Thanks!

[Update] some things I'm thinking of trying next:

jabelone commented 2 years ago

Heya, sorry for the issues. I'll do my best to help :)

I can see this line in your error log which means it can't find the /usr/src/data/setupcomplete file inside the container (there should be a mapped volume which contains this and the sqlite database after a first run): INFO: Detected a first time run. Populating the database with defaults.

Maybe double check where you're running the updated container from first. I don't think this would cause your other issue, but best to rule it out. :)

That being said, here is what I've found regarding your migration issue: At first I couldn't reproduce it; from an older database backup I have of my production instance, nor from an empty db. However, I think I remember hitting the same error during development one time and it was due to inconsistent data in the database so I did some digging and can now reproduce it. It looks like that inconsistent data is caused by the limitation of sqlite that you identified.

To fix it, use an sqlite GUI tool like TablePlus or sqlitebrowser (or CLI if you're crazy 😝). Have a look in the profile_profile_groups table. You'll find one or more entries with a group_id of 1 (according to your error). You can remove these and the migration should proceed without error.

Let me know if that works, and of course take a database backup first. :)

rechner commented 2 years ago

Thanks for the quick reply-

Since we're not open yet, I didn't have any attachment to the original database, and tried to spin up a new instance to check if that would work.

Per your advice, I decided to try deleting those entries on the old sqlite database and that worked! I'll give a spin up from scratch another try tomorrow, still interested in figuring out why I can't get mysql working :thinking:

Thanks again for the help.

jabelone commented 2 years ago

Perfect! I'll close this issue for now then, but please open another one if you run into any more issues :)