diff --git a/backend/app.py b/backend/app.py
index 76dddf1ae99111eda75ab54f0c9a8d598ddd9e90..067d4e14b3cbf94dde567d2e8819cbc504255928 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -1,8 +1,9 @@
 from flask import Flask, jsonify
 from flask_cors import CORS
 from flask_jwt_extended import JWTManager
-from flask_migrate import Migrate
+import flask_migrate
 from jsonschema.exceptions import ValidationError
+from NamedAtomicLock import NamedAtomicLock
 from werkzeug.exceptions import BadRequest
 
 # These imports are required
@@ -32,8 +33,11 @@ from helpers import (
     unauthorized_error,
 )
 
+import cluster_config
 from config import *
 import logging
+import migration_reset
+import sys
 
 # Configure logging.
 from logging.config import dictConfig
@@ -59,12 +63,41 @@ app.config["SECRET_KEY"] = SECRET_KEY
 app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = SQLALCHEMY_TRACK_MODIFICATIONS
 
+app.logger.setLevel(logging.INFO)
+
 cors = CORS(app)
-Migrate(app, db)
-db.init_app(app)
 
+db.init_app(app)
 
-app.logger.setLevel(logging.INFO)
+# We'll now perform some initialization routines. Because these have to be done
+# once at startup, not for every gunicorn worker, we take a machine-wide lock
+# for this.
+init_lock = NamedAtomicLock('dashboard_init')
+if init_lock.acquire():
+    try:
+        with app.app_context():
+            # We have reset the alembic migration history at Stackspin version 2.2.
+            # This checks whether we need to prepare the database to follow that
+            # change.
+            migration_reset.reset()
+        flask_migrate.Migrate(app, db)
+        try:
+            with app.app_context():
+                flask_migrate.upgrade()
+        except Exception as e:
+            app.logger.info(f"upgrade failed: {type(e)}: {e}")
+            sys.exit(2)
+
+        # We need this app context in order to talk the database, which is managed by
+        # flask-sqlalchemy, which assumes a flask app context.
+        with app.app_context():
+            # Load the list of apps from a configmap and store any missing ones in the
+            # database.
+            cluster_config.populate_apps()
+            # Same for the list of oauthclients.
+            cluster_config.populate_oauthclients()
+    finally:
+        init_lock.release()
 
 app.register_blueprint(api_v1)
 app.register_blueprint(web)
diff --git a/backend/areas/apps/models.py b/backend/areas/apps/models.py
index ef930501a9d8be66a5d27114531edd57477c2574..069c73751643f42af0619e65c7d6425d4802138d 100644
--- a/backend/areas/apps/models.py
+++ b/backend/areas/apps/models.py
@@ -30,6 +30,12 @@ class App(db.Model):
     # URL is stored in a configmap (see get_url)
     url = db.Column(String(length=128), unique=False)
 
+    def __init__(self, slug, name, external=False, url=None):
+        self.slug = slug
+        self.name = name
+        self.external = external
+        self.url = url
+
     def __repr__(self):
         return f"{self.id} <{self.name}>"
 
@@ -303,6 +309,7 @@ class OAuthClientApp(db.Model):  # pylint: disable=too-few-public-methods
     This mapping exists so that
     * you can have a different name for the OAuth client than for the app, and
     * you can have multiple OAuth clients that belong to the same app.
+    Also, some apps might have no OAuth client at all.
     """
 
     __tablename__ = "oauthclient_app"
diff --git a/backend/cluster_config.py b/backend/cluster_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..20ec9e98023dfd01b46a30492f74057aea1a1aed
--- /dev/null
+++ b/backend/cluster_config.py
@@ -0,0 +1,83 @@
+from database import db
+from areas.apps.models import App, OAuthClientApp
+import helpers.kubernetes as k8s
+
+import logging
+import yaml
+
+# Read in two configmaps from the cluster, which specify which apps should be
+# present in the database.
+def populate_apps():
+    logging.info("cluster_config: populating apps")
+    database_apps = {}
+    for app in App.query.all():
+        slug = app.slug
+        database_apps[slug] = app
+        logging.info(f"database app: {slug}")
+    _populate_apps_from(database_apps, "stackspin-apps")
+    _populate_apps_from(database_apps, "stackspin-apps-custom")
+
+# Read a list of apps from a configmap. Check if they are already present in
+# the database, and if not, add missing ones there. Properties `name`,
+# `external` and `url` can be specified in yaml format in the configmap value
+# contents.
+def _populate_apps_from(database_apps, configmap_name):
+    cm_apps = k8s.get_kubernetes_config_map_data(configmap_name, "flux-system")
+    if cm_apps is None:
+        logging.info(f"Could not find configmap '{configmap_name}' in namespace 'flux-system'; ignoring.")
+    else:
+        for app_slug, app_data in cm_apps.items():
+            logging.info(f"configmap app: {app_slug}")
+            if app_slug in database_apps:
+                logging.info(f"  already present in database")
+            else:
+                logging.info(f"  not present in database, adding!")
+                data = yaml.safe_load(app_data)
+                name = data["name"]
+                logging.info(f"  name: {name}")
+                external = data.get("external", False)
+                logging.info(f"  type external: {type(external)}")
+                logging.info(f"  external: {external}")
+                url = data.get("url", None)
+                logging.info(f"  url: {url}")
+                new_app = App(slug=app_slug, name=name, external=external, url=url)
+                db.session.add(new_app)
+        db.session.commit()
+
+# Read in two configmaps from the cluster, which specify which oauthclients
+# should be present in the database.
+def populate_oauthclients():
+    logging.info("cluster_config: populating oauthclients")
+    database_oauthclients = {}
+    for client in OAuthClientApp.query.all():
+        id = client.oauthclient_id
+        database_oauthclients[id] = client
+        logging.info(f"database oauthclient: {id}")
+    _populate_oauthclients_from(database_oauthclients, "stackspin-oauthclients")
+    _populate_oauthclients_from(database_oauthclients, "stackspin-oauthclients-custom")
+
+# Read a list of oauthclients from a configmap. Check if they are already
+# present in the database, and if not, add missing ones there. The value of the
+# mapping is taken to be the slug of the app the oauthclient belongs to.
+def _populate_oauthclients_from(database_oauthclients, configmap_name):
+    cm_oauthclients = k8s.get_kubernetes_config_map_data(configmap_name, "flux-system")
+    if cm_oauthclients is None:
+        logging.info(f"Could not find configmap '{configmap_name}' in namespace 'flux-system'; ignoring.")
+    else:
+        for client_id, client_app in cm_oauthclients.items():
+            logging.info(f"configmap oauthclient: {client_id}")
+            if client_id in database_oauthclients:
+                logging.info(f"  already present in database")
+            else:
+                logging.info(f"  not present in database, adding!")
+                # Take the value of the configmap mapping (`client_app`) and
+                # interpret it as the slug of the app that this oauthclient
+                # belongs to.
+                app = App.query.filter_by(slug=client_app).first()
+                if not app:
+                    logging.error(f"  could not find app with slug {client_app}")
+                    continue
+                new_client = OAuthClientApp(oauthclient_id=client_id, app_id=app.id)
+                logging.info(f"  new oauth client: {new_client}")
+                db.session.add(new_client)
+        db.session.commit()
diff --git a/backend/migration_reset.py b/backend/migration_reset.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f674c75ecff4e31ccf744818978580270e4ef48
--- /dev/null
+++ b/backend/migration_reset.py
@@ -0,0 +1,34 @@
+from sqlalchemy import exc
+
+from database import db
+import logging
+
+# We "reset" the alembic version history for Stackspin 2.2, to clean up our old
+# mess of database migrations a bit, and in particular to make the transition
+# easier to moving the source of truth for some of the data (list of apps) out
+# of the database and into configmaps. This function deals with older clusters
+# that have to be led through this transition. To determine if we need to do
+# anything, we look at the `alembic_version` value in the database. If it's a
+# legacy version, we delete the table so the alembic migration will view the
+# database as "empty" and perform all new migrations on it. The new initial
+# migration will have to handle that case specially, by checking if any tables
+# already exist, and not do anything in that case.
+def reset():
+    logging.info("Checking if alembic version needs to be reset.")
+    version = None
+    try:
+        result = db.session.execute("select version_num from alembic_version")
+        for row in result:
+            version = row[0]
+    except exc.ProgrammingError:
+        # We assume this means the alembic_version table doesn't exist, which
+        # is expected for new clusters.
+        pass
+    logging.info(f"alembic version: {version}")
+    legacy_versions = ["fc0892d07771", "3fa0c38ea1ac", "e08df0bef76f", "b514cca2d47b", "5f462d2d9d25", "27761560bbcb"]
+    if version in legacy_versions:
+        logging.info("This is an old version: resetting.")
+        db.session.execute("drop table alembic_version")
+    else:
+        logging.info("This is not a known legacy version: not resetting.")
+        
diff --git a/backend/migrations/alembic.ini b/backend/migrations/alembic.ini
index ec9d45c26a6bb54e833fd4e6ce2de29343894f4b..ef355cdd0bc77377516a78e5f891f20b8573b403 100644
--- a/backend/migrations/alembic.ini
+++ b/backend/migrations/alembic.ini
@@ -8,43 +8,3 @@
 # the 'revision' command, regardless of autogenerate
 # revision_environment = false
 
-
-# Logging configuration
-[loggers]
-keys = root,sqlalchemy,alembic,flask_migrate
-
-[handlers]
-keys = console
-
-[formatters]
-keys = generic
-
-[logger_root]
-level = WARN
-handlers = console
-qualname =
-
-[logger_sqlalchemy]
-level = WARN
-handlers =
-qualname = sqlalchemy.engine
-
-[logger_alembic]
-level = INFO
-handlers =
-qualname = alembic
-
-[logger_flask_migrate]
-level = INFO
-handlers =
-qualname = flask_migrate
-
-[handler_console]
-class = StreamHandler
-args = (sys.stderr,)
-level = NOTSET
-formatter = generic
-
-[formatter_generic]
-format = %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
diff --git a/backend/migrations/env.py b/backend/migrations/env.py
index 68feded2a040005310d770ac7136b2e4ff8a6312..2a7628b80babbf6b5a2777efe7e02c5c115c391c 100644
--- a/backend/migrations/env.py
+++ b/backend/migrations/env.py
@@ -13,7 +13,9 @@ config = context.config
 
 # Interpret the config file for Python logging.
 # This line sets up loggers basically.
-fileConfig(config.config_file_name)
+# We commented this out, because we want to configure logging in the app
+# itself, not here.
+# fileConfig(config.config_file_name)
 logger = logging.getLogger('alembic.env')
 
 # add your model's MetaData object here
diff --git a/backend/migrations/versions/27761560bbcb_.py b/backend/migrations/versions/27761560bbcb_.py
deleted file mode 100644
index baa80e4969b00d9fcb03b6048d254703d6a07759..0000000000000000000000000000000000000000
--- a/backend/migrations/versions/27761560bbcb_.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""empty message
-
-Revision ID: 27761560bbcb
-Revises: 
-Create Date: 2021-12-21 06:07:14.857940
-
-"""
-import sqlalchemy as sa
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision = "27761560bbcb"
-down_revision = None
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.create_table(
-        "app",
-        sa.Column("id", sa.Integer(), nullable=False),
-        sa.Column("name", sa.String(length=64), nullable=True),
-        sa.Column("slug", sa.String(length=64), nullable=True),
-        sa.PrimaryKeyConstraint("id"),
-        sa.UniqueConstraint("slug"),
-    )
-    op.create_table(
-        "app_role",
-        sa.Column("user_id", sa.String(length=64), nullable=False),
-        sa.Column("app_id", sa.Integer(), nullable=False),
-        sa.Column("role", sa.String(length=64), nullable=True),
-        sa.ForeignKeyConstraint(
-            ["app_id"],
-            ["app.id"],
-        ),
-        sa.PrimaryKeyConstraint("user_id", "app_id"),
-    )
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_table("app_role")
-    op.drop_table("app")
-    # ### end Alembic commands ###
diff --git a/backend/migrations/versions/3fa0c38ea1ac_add_velero_as_app.py b/backend/migrations/versions/3fa0c38ea1ac_add_velero_as_app.py
deleted file mode 100644
index 5caae97d62acdabc750580389246cbc376162c35..0000000000000000000000000000000000000000
--- a/backend/migrations/versions/3fa0c38ea1ac_add_velero_as_app.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""add-velero-as-app
-
-Revision ID: 3fa0c38ea1ac
-Revises: e08df0bef76f
-Create Date: 2022-10-13 09:40:44.290319
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = '3fa0c38ea1ac'
-down_revision = 'e08df0bef76f'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # Add monitoring app
-    op.execute(f'INSERT IGNORE INTO app (`name`, `slug`) VALUES ("Velero","velero")')
-
-
-def downgrade():
-    pass
diff --git a/backend/migrations/versions/5f462d2d9d25_convert_role_column_to_table.py b/backend/migrations/versions/5f462d2d9d25_convert_role_column_to_table.py
deleted file mode 100644
index 53a8a1d5defc9223443200f79649c703e6b4025c..0000000000000000000000000000000000000000
--- a/backend/migrations/versions/5f462d2d9d25_convert_role_column_to_table.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""convert role column to table
-
-Revision ID: 5f462d2d9d25
-Revises: 27761560bbcb
-Create Date: 2022-04-13 15:00:27.182898
-
-"""
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = "5f462d2d9d25"
-down_revision = "27761560bbcb"
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    role_table = op.create_table(
-        "role",
-        sa.Column("id", sa.Integer(), nullable=False),
-        sa.Column("name", sa.String(length=64), nullable=True),
-        sa.PrimaryKeyConstraint("id"),
-    )
-    op.add_column("app_role", sa.Column("role_id", sa.Integer(), nullable=True))
-    op.create_foreign_key(None, "app_role", "role", ["role_id"], ["id"])
-    # ### end Alembic commands ###
-
-    # Insert default role "admin" as ID 1
-    op.execute(sa.insert(role_table).values(id=1,name="admin"))
-    # Set role_id 1 to all current "admin" users
-    op.execute("UPDATE app_role SET role_id = 1 WHERE role = 'admin'")
-
-    # Drop old column
-    op.drop_column("app_role", "role")
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column(
-        "app_role", sa.Column("role", mysql.VARCHAR(length=64), nullable=True)
-    )
-    op.drop_constraint(None, "app_role", type_="foreignkey")
-    op.drop_column("app_role", "role_id")
-    op.drop_table("role")
-    # ### end Alembic commands ###
diff --git a/backend/migrations/versions/7d27395c892a_new_migration.py b/backend/migrations/versions/7d27395c892a_new_migration.py
new file mode 100644
index 0000000000000000000000000000000000000000..94a1d844c0918712970147183025150a28ccd37c
--- /dev/null
+++ b/backend/migrations/versions/7d27395c892a_new_migration.py
@@ -0,0 +1,72 @@
+"""Initial version after history reset: Create tables and fill the "role" one
+
+Revision ID: 7d27395c892a
+Revises:
+Create Date: 2023-01-18 14:48:23.996261
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import mysql
+from sqlalchemy.engine.reflection import Inspector
+
+# revision identifiers, used by Alembic.
+revision = '7d27395c892a'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+conn = op.get_bind()
+inspector = Inspector.from_engine(conn)
+tables = inspector.get_table_names()
+
+def upgrade():
+    if "app" not in tables:
+        op.create_table(
+            "app",
+            sa.Column("id", sa.Integer(), nullable=False),
+            sa.Column("name", sa.String(length=64), nullable=False),
+            sa.Column("slug", sa.String(length=64), nullable=False),
+            sa.Column("external", sa.Boolean(), server_default='0', nullable=False),
+            sa.Column("url", sa.String(length=128), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+            sa.UniqueConstraint("slug"),
+        )
+
+    if "role" not in tables:
+        op.create_table(
+            "role",
+            sa.Column("id", sa.Integer(), nullable=False),
+            sa.Column("name", sa.String(length=64), nullable=True),
+            sa.PrimaryKeyConstraint("id")
+        )
+        op.execute("INSERT INTO `role` (id, `name`) VALUES (1, 'admin')")
+        op.execute("INSERT INTO `role` (id, `name`) VALUES (2, 'user')")
+        op.execute("INSERT INTO `role` (id, `name`) VALUES (3, 'no access')")
+
+    if "app_role" not in tables:
+        op.create_table(
+            "app_role",
+            sa.Column("user_id", sa.String(length=64), nullable=False),
+            sa.Column("app_id", sa.Integer(), nullable=False),
+            sa.Column("role_id", sa.Integer(), nullable=True),
+            sa.PrimaryKeyConstraint("user_id", "app_id"),
+            sa.ForeignKeyConstraint(["app_id"],["app.id"]),
+            sa.ForeignKeyConstraint(["role_id"],["role.id"])
+        )
+
+    if "oauthclient_app" not in tables:
+        op.create_table('oauthclient_app',
+            sa.Column('oauthclient_id', mysql.VARCHAR(length=64), nullable=False),
+            sa.Column('app_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
+            sa.PrimaryKeyConstraint('oauthclient_id'),
+            sa.ForeignKeyConstraint(['app_id'], ['app.id']),
+            mysql_default_charset='utf8mb3',
+            mysql_engine='InnoDB'
+        )
+
+def downgrade():
+    op.drop_table("oauthclient_app")
+    op.drop_table("app_role")
+    op.drop_table("role")
+    op.drop_table("app")
\ No newline at end of file
diff --git a/backend/migrations/versions/b514cca2d47b_add_user_role.py b/backend/migrations/versions/b514cca2d47b_add_user_role.py
deleted file mode 100644
index 058694200fe0e2c1a9b6ca82abf9c023e5562b40..0000000000000000000000000000000000000000
--- a/backend/migrations/versions/b514cca2d47b_add_user_role.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""update apps and add 'user' and 'no access' role
-
-Revision ID: b514cca2d47b
-Revises: 5f462d2d9d25
-Create Date: 2022-06-08 17:24:51.305129
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-# revision identifiers, used by Alembic.
-revision = 'b514cca2d47b'
-down_revision = '5f462d2d9d25'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### end Alembic commands ###
-
-    # Check and update app table in DB
-    apps = {
-        "dashboard": "Dashboard",
-        "wekan": "Wekan",
-        "wordpress": "WordPress",
-        "nextcloud": "Nextcloud",
-        "zulip": "Zulip"
-    }
-    # app table
-    app_table = sa.table('app', sa.column('id', sa.Integer), sa.column(
-        'name', sa.String), sa.column('slug', sa.String))
-
-    existing_apps = op.get_bind().execute(app_table.select()).fetchall()
-    existing_app_slugs = [app['slug'] for app in existing_apps]
-    for app_slug in apps.keys():
-        if app_slug in existing_app_slugs:
-            op.execute(f'UPDATE app SET `name` = "{apps.get(app_slug)}" WHERE slug = "{app_slug}"')
-        else:
-            op.execute(f'INSERT INTO app (`name`, slug) VALUES ("{apps.get(app_slug)}","{app_slug}")')
-
-    # Fetch all apps including newly created
-    existing_apps = op.get_bind().execute(app_table.select()).fetchall()
-    # Insert role "user" as ID 2
-    op.execute("INSERT INTO `role` (id, `name`) VALUES (2, 'user')")
-    # Insert role "no access" as ID 3
-    op.execute("INSERT INTO `role` (id, `name`) VALUES (3, 'no access')")
-    # Set role_id 2 to all current "user" users which by have NULL role ID
-    op.execute("UPDATE app_role SET role_id = 2 WHERE role_id IS NULL")
-
-    # Add 'no access' role for all users that don't have any roles for specific apps
-    app_roles_table = sa.table('app_role', sa.column('user_id', sa.String), sa.column(
-        'app_id', sa.Integer), sa.column('role_id', sa.Integer))
-    
-    app_ids = [app['id'] for app in existing_apps]
-    app_roles = op.get_bind().execute(app_roles_table.select()).fetchall()
-    user_ids = set([app_role['user_id'] for app_role in app_roles])
-
-    for user_id in user_ids:
-        existing_user_app_ids = [x['app_id'] for x in list(filter(lambda role: role['user_id'] == user_id, app_roles))]
-        missing_user_app_ids = [x for x in app_ids if x not in existing_user_app_ids]
-
-        if len(missing_user_app_ids) > 0:
-            values = [{'user_id': user_id, 'app_id': app_id, 'role_id': 3} for app_id in missing_user_app_ids]
-            op.bulk_insert(app_roles_table, values)
-
-
-def downgrade():
-    # Revert all users role_id to NULL where role is 'user'
-    op.execute("UPDATE app_role SET role_id = NULL WHERE role_id = 2")
-    # Delete role 'user' from roles
-    op.execute("DELETE FROM `role` WHERE id = 2")
-
-    # Delete all user app roles where role is 'no access' with role_id 3
-    op.execute("DELETE FROM app_role WHERE role_id = 3")
-    # Delete role 'no access' from roles
-    op.execute("DELETE FROM `role` WHERE id = 3")
diff --git a/backend/migrations/versions/e08df0bef76f_.py b/backend/migrations/versions/e08df0bef76f_.py
deleted file mode 100644
index 005833fb12e38ce6241720a520f160316c53d251..0000000000000000000000000000000000000000
--- a/backend/migrations/versions/e08df0bef76f_.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""Add fields for external apps
-
-Revision ID: e08df0bef76f
-Revises: b514cca2d47b
-Create Date: 2022-09-23 16:38:06.557307
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'e08df0bef76f'
-down_revision = 'b514cca2d47b'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('app', sa.Column('external', sa.Boolean(), server_default='0', nullable=False))
-    op.add_column('app', sa.Column('url', sa.String(length=128), nullable=True))
-    # ### end Alembic commands ###
-
-    # Add monitoring app
-    op.execute(f'INSERT IGNORE INTO app (`name`, `slug`) VALUES ("Monitoring","monitoring")')
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_column('app', 'url')
-    op.drop_column('app', 'external')
-    # ### end Alembic commands ###
diff --git a/backend/migrations/versions/fc0892d07771_add_oauthclient_app_table.py b/backend/migrations/versions/fc0892d07771_add_oauthclient_app_table.py
deleted file mode 100644
index be0ddf02201f097547d2a69346aa1479dabd976c..0000000000000000000000000000000000000000
--- a/backend/migrations/versions/fc0892d07771_add_oauthclient_app_table.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Add oauthclient_app table
-
-Revision ID: fc0892d07771
-Revises: 3fa0c38ea1ac
-Create Date: 2022-11-02 09:52:09.510764
-
-"""
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = 'fc0892d07771'
-down_revision = '3fa0c38ea1ac'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    oauthclient_app_table = op.create_table('oauthclient_app',
-        sa.Column('oauthclient_id', mysql.VARCHAR(length=64), nullable=False),
-        sa.Column('app_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
-        sa.ForeignKeyConstraint(['app_id'], ['app.id'], name='oauthclient_app_fk_app_id'),
-        sa.PrimaryKeyConstraint('oauthclient_id'),
-        mysql_default_charset='utf8mb3',
-        mysql_engine='InnoDB'
-    )
-    values = [
-        {"oauthclient_id": "dashboard"            , "app_id": 1},
-        {"oauthclient_id": "wekan"                , "app_id": 2},
-        {"oauthclient_id": "wordpress"            , "app_id": 3},
-        {"oauthclient_id": "nextcloud"            , "app_id": 4},
-        {"oauthclient_id": "zulip"                , "app_id": 5},
-        {"oauthclient_id": "kube-prometheus-stack", "app_id": 6},
-    ]
-    op.bulk_insert(oauthclient_app_table, values)
-
-def downgrade():
-    op.drop_table('oauthclient_app')
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 44114da26d1c4a6e53eb3c564fc71b4ed00c32b0..fc2836ad096a8f50d217569cc0a9b8eccdc5db34 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -21,6 +21,7 @@ jinja2-base64-filters==0.1.4
 kubernetes==24.2.0
 MarkupSafe==2.1.1
 mypy-extensions==0.4.3
+NamedAtomicLock==1.1.3
 oauthlib==3.2.0
 ory-kratos-client==0.11.0
 ory-hydra-client==1.11.8
@@ -30,6 +31,7 @@ pycparser==2.21
 PyJWT==2.3.0
 pymysql==1.0.2
 pyrsistent==0.18.1
+PyYAML==6.0
 regex==2022.3.15
 requests==2.27.1
 requests-oauthlib==1.3.1
diff --git a/deployment/helmchart/templates/job-initialize-user.yaml b/deployment/helmchart/templates/job-initialize-user.yaml
index 32910dbc7b17209e8a3a74fe59008c34e34aa247..4cf77350d3cbfbd3b2defeb9e3f732397d8ab95b 100644
--- a/deployment/helmchart/templates/job-initialize-user.yaml
+++ b/deployment/helmchart/templates/job-initialize-user.yaml
@@ -21,6 +21,7 @@ spec:
         component: dashboard
     spec:
       restartPolicy: Never
+      serviceAccountName: {{ include "dashboard.serviceAccountName" . }}
       containers:
       - name: {{ .Chart.Name }}-login-create-admin
         image: {{ template "backend.image" . }}
diff --git a/deployment/helmchart/values.yaml b/deployment/helmchart/values.yaml
index a7be717951b75c0be3a768e79973ad3692d43d11..35ae6d4fd479970e5c0a10d3c338706b66c7e915 100644
--- a/deployment/helmchart/values.yaml
+++ b/deployment/helmchart/values.yaml
@@ -68,7 +68,7 @@ dashboard:
   image:
     registry: open.greenhost.net:4567
     repository: stackspin/dashboard/dashboard
-    tag: 0.5.2
+    tag: 0.6.0
     digest: ""
     ## Optionally specify an array of imagePullSecrets.
     ## Secrets must be manually created in the namespace.
@@ -236,7 +236,7 @@ backend:
   image:
     registry: open.greenhost.net:4567
     repository: stackspin/dashboard/dashboard-backend
-    tag: 0.5.2
+    tag: 0.6.0
     digest: ""
     ## Optionally specify an array of imagePullSecrets.
     ## Secrets must be manually created in the namespace.
diff --git a/docker-compose.yml b/docker-compose.yml
index c5c20e4f7abfb9fcdb8fd44f85ab102c4a555229..cfab7512f853d5cfd0b44e507548180deb883a99 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -57,7 +57,7 @@ services:
       - kube_port_mysql
     entrypoint: ["bash", "-c", "flask run --host $$(hostname -i)"]
   kube_port_kratos_admin:
-    image: bitnami/kubectl:1.25.5
+    image: bitnami/kubectl:1.26.1
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     expose:
       - 8000
@@ -65,7 +65,7 @@ services:
       - "$KUBECONFIG:/.kube/config"
     entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address $$(hostname -i) service/kratos-admin 8000:80"]
   kube_port_hydra_admin:
-    image: bitnami/kubectl:1.25.5
+    image: bitnami/kubectl:1.26.1
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     expose:
       - 4445
@@ -73,7 +73,7 @@ services:
       - "$KUBECONFIG:/.kube/config"
     entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address $$(hostname -i) service/hydra-admin 4445:4445"]
   kube_port_kratos_public:
-    image: bitnami/kubectl:1.25.5
+    image: bitnami/kubectl:1.26.1
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     ports:
       - "8080:8080"
@@ -83,7 +83,7 @@ services:
       - "$KUBECONFIG:/.kube/config"
     entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address 0.0.0.0 service/kratos-public 8080:80"]
   kube_port_mysql:
-    image: bitnami/kubectl:1.25.5
+    image: bitnami/kubectl:1.26.1
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     expose:
       - 3306