Files
alt-mysql/multi-database-plan.md
2026-07-04 15:48:19 +02:00

33 KiB

Multi-Database Installation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Refactor alt-mysql so one DirectAdmin plugin can install, discover, secure, operate, back up, restore, and expose multiple MariaDB versions as separate local instances.

Architecture: Introduce an explicit MariaDB instance registry. The existing /usr/local/directadmin/conf/alt-mariadb.env becomes the registry/index file, while every installed version gets its own per-instance env file, systemd unit, datadir, socket, pid file, and server config. /usr/local/directadmin/conf/alt-mysql.conf and /usr/local/directadmin/conf/alt-my.cnf remain shared credential-only files; runtime code appends the selected instance socket/port explicitly for every operation.

Tech Stack: Bash, PHP 8-compatible plugin code, mysqli, DirectAdmin plugin handlers, systemd units, MariaDB binary tarballs, CSF allow-file integration, DirectAdmin backup/restore hooks.


Current State Summary

The plugin currently assumes one alternative MariaDB instance:

  • scripts/install_db.sh writes one /usr/local/directadmin/conf/alt-mariadb.env, one /etc/alt-mariadb.cnf, one alt-mariadb systemd service, one datadir, one socket, one port, and one shared client defaults file that also contains the socket.
  • scripts/setup/mysql_common.sh resolves a single runtime and every shell integration uses that runtime.
  • exec/bootstrap.php creates one MySQLService from one credential file.
  • exec/lib/MySQLService.php stores metadata tables inside that one MariaDB instance and all database/user/host operations run against it.
  • exec/handlers/index.php lists databases from one instance, prints one server version, and creates databases without a version selector.
  • BackupQueueService, backup_job.sh, restore_job.sh, DA backup/restore hooks, phpMyAdmin SSO, and CSF host sync all assume that DB_NAME is enough to identify the target.

This must change before multiple versions are safe. Without an instance identifier, a backup, restore, deletion, phpMyAdmin login, or host-rule sync can target the wrong MariaDB instance.


Target Runtime Model

Instance Identity

Use one stable instance id per MariaDB series. The id is the version string accepted by the installer, for example:

  • 10.11
  • 11.4
  • 11.8

For shell-safe names, derive a slug by replacing unsupported characters with _ only where needed:

  • systemd unit: alt-mariadb-10.11.service
  • config file: /etc/alt-mariadb-10.11.cnf
  • datadir: /var/lib/alt-mariadb-10.11
  • socket: /var/lib/alt-mariadb-10.11/mariadb.sock
  • pid file: /var/lib/alt-mariadb-10.11/mariadb.pid
  • log dir: /var/log/alt-mariadb-10.11
  • runtime dir: /run/alt-mariadb-10.11
  • binary dir: /usr/local/mariadb-10.11

The plan intentionally allows only one active instance per version series. Installing 11.4 twice should fail with a clear message unless the existing 11.4 instance is being repaired.

Shared Credentials

Keep credentials shared across all alternative MariaDB instances:

/usr/local/directadmin/conf/alt-mysql.conf

host=
user=da_admin
passwd=<shared secret>
database=mysql

/usr/local/directadmin/conf/alt-my.cnf

[client]
user="da_admin"
password="<shared secret>"

Do not store port= or socket= in the shared client file. Every call must pass --socket="$INSTANCE_SOCKET" or --protocol=TCP --host=127.0.0.1 --port="$INSTANCE_PORT" explicitly. This avoids silently connecting to the first installed version when the user selected another version.

On first install, initialize the shared credential files from the current DirectAdmin native MySQL credentials, as the existing script does. On every later install, detect that /usr/local/directadmin/conf/alt-mariadb.env already exists and reuse /usr/local/directadmin/conf/alt-mysql.conf and /usr/local/directadmin/conf/alt-my.cnf; do not read or overwrite credentials from /usr/local/directadmin/conf/mysql.conf.

The shared password must be applied to:

  • the shared admin user from alt-mysql.conf on every installed instance,
  • root@localhost on every installed instance,
  • root@127.0.0.1 where the account exists or is created.

Registry Files

Keep /usr/local/directadmin/conf/alt-mariadb.env as the canonical registry/index file.

Example:

# alt-mysql MariaDB instance registry.
ALT_MARIADB_REGISTRY_VERSION='2'
ALT_MARIADB_DEFAULT_INSTANCE='10.11'
ALT_MARIADB_INSTANCES='10.11 11.4'
ALT_MARIADB_DIR='/usr/local/directadmin/conf/alt-mariadb.d'
ALT_MYSQL_CONF='/usr/local/directadmin/conf/alt-mysql.conf'
ALT_MY_CNF='/usr/local/directadmin/conf/alt-my.cnf'

Store each instance in /usr/local/directadmin/conf/alt-mariadb.d/<instance-id>.env.

Example:

INSTANCE_ID='11.4'
MARIADB_VERSION='11.4'
MARIADB_RELEASE='11.4.8'
SERVICE_NAME='alt-mariadb-11.4'
MARIADB_DIR='/usr/local/mariadb-11.4'
DATADIR='/var/lib/alt-mariadb-11.4'
PORT='3020'
SOCKET='/var/lib/alt-mariadb-11.4/mariadb.sock'
PID_FILE='/var/lib/alt-mariadb-11.4/mariadb.pid'
INSTANCE_CNF='/etc/alt-mariadb-11.4.cnf'
SERVICE_FILE='/etc/systemd/system/alt-mariadb-11.4.service'
LOG_DIR='/var/log/alt-mariadb-11.4'
RUN_DIR='/run/alt-mariadb-11.4'
SYSTEM_USER='mysql'
SYSTEM_GROUP='mysql'
BIND_ADDRESS='127.0.0.1'
ALT_MYSQL_CONF='/usr/local/directadmin/conf/alt-mysql.conf'
ALT_MY_CNF='/usr/local/directadmin/conf/alt-my.cnf'

Migration rule for existing single-instance installs:

  1. If alt-mariadb.env contains SERVICE_NAME=, MARIADB_VERSION=, PORT=, and SOCKET=, treat it as legacy format.
  2. Create /usr/local/directadmin/conf/alt-mariadb.d/<MARIADB_VERSION>.env from those values.
  3. Rewrite alt-mariadb.env as the registry/index file.
  4. Keep a timestamped backup of the legacy file before rewriting it.

Data And Metadata Model

Metadata Tables

Keep metadata tables inside every MariaDB instance. This is the safest model because each MariaDB instance can operate independently and there is no single metadata database that breaks the whole fleet.

Add instance_id to every plugin metadata table whose rows refer to user databases or roles:

ALTER TABLE da_plugin_database_owners
  ADD COLUMN instance_id VARCHAR(32) NOT NULL DEFAULT '';

ALTER TABLE da_plugin_grant_profiles
  ADD COLUMN instance_id VARCHAR(32) NOT NULL DEFAULT '';

ALTER TABLE da_plugin_access_hosts
  ADD COLUMN instance_id VARCHAR(32) NOT NULL DEFAULT '';

ALTER TABLE da_plugin_sso_accounts
  ADD COLUMN instance_id VARCHAR(32) NOT NULL DEFAULT '';

Then migrate primary keys:

  • da_plugin_database_owners: primary key becomes (instance_id, db_name).
  • da_plugin_grant_profiles: primary key becomes (instance_id, db_name, role_name).
  • da_plugin_access_hosts: primary key becomes (instance_id, role_name, host_pattern).
  • da_plugin_sso_accounts: primary key becomes (instance_id, role_name, host_pattern).

For a fresh instance, create the schema with instance_id already populated. For a migrated single-instance schema, fill instance_id with the instance id currently being loaded.

Database Name Uniqueness

Recommended first implementation: database names remain globally unique across all plugin-managed instances.

Reasoning:

  • It avoids ambiguous DirectAdmin restore behavior.
  • It keeps CMS credential changes simple.
  • It avoids accidental phpMyAdmin, backup, restore, and delete actions against the wrong version.
  • It keeps existing user-facing workflows mostly unchanged.

Implementation rule:

  • Before creating a database on selected version X, query every installed instance and reject the name if it exists anywhere.
  • Later, if duplicate names across versions are required, all forms, job files, URLs, payload manifests, and locks already need instance_id; this plan prepares that path, but the first production-safe step should enforce global uniqueness.

Installer Refactor

Required Behavior

scripts/install_db.sh remains the entry point:

./scripts/install_db.sh 11.4 3020

It must:

  • require both version and port,
  • derive datadir as /var/lib/alt-mariadb-$version,
  • derive socket as /var/lib/alt-mariadb-$version/mariadb.sock,
  • derive pid file as /var/lib/alt-mariadb-$version/mariadb.pid,
  • derive config file as /etc/alt-mariadb-$version.cnf,
  • derive service name as alt-mariadb-$version,
  • reuse shared root/admin password from alt-mysql.conf if the registry already exists,
  • write or update only the selected instance env file,
  • update ALT_MARIADB_INSTANCES in the registry without removing existing instances,
  • run non-interactive secure-install SQL for the selected instance,
  • run scripts/install.sh after successful install unless SKIP_PLUGIN_INSTALL_REFRESH=1.

Non-Interactive Secure Installation

After the instance starts and after shared admin/root credentials are set, run a function equivalent to mariadb-secure-installation without prompts.

The SQL must be version-tolerant:

DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db = 'test' OR Db LIKE 'test\\_%';
FLUSH PRIVILEGES;

For anonymous users and remote root accounts:

  • Prefer mysql.global_priv when it exists.
  • Fall back to mysql.user when mysql.global_priv does not exist.
  • Remove anonymous users.
  • Remove root accounts whose host is not localhost, 127.0.0.1, or ::1.
  • Keep local root accounts and set their password to the shared secret.

The secure SQL function must log the actions but never print the password.

Installer Tests

Add tests before implementation:

  • tests/install_db_multi_instance_test.sh

    • validates derived paths for 11.4 3020,
    • validates service name alt-mariadb-11.4,
    • validates no default /var/lib/mariadb_data,
    • validates registry update functions exist,
    • validates secure-install function exists and references anonymous users, test database, and remote root cleanup.
  • tests/registry_migration_test.sh

    • feeds a legacy alt-mariadb.env,
    • expects a registry file plus alt-mariadb.d/<version>.env,
    • expects the original file backup to be created.

PHP Runtime Refactor

New Classes

Create exec/lib/MariaDbInstance.php.

Responsibilities:

  • hold instanceId, version, release, serviceName, basedir, datadir, port, socket, pidFile, instanceCnf, serviceFile, logDir, runDir,
  • expose clientCredentials(array $sharedCredentials): array,
  • expose label(): string for UI display.

Create exec/lib/MariaDbInstanceRegistry.php.

Responsibilities:

  • load /usr/local/directadmin/conf/alt-mariadb.env,
  • migrate legacy single-instance env format,
  • load all /usr/local/directadmin/conf/alt-mariadb.d/*.env,
  • return all(): array,
  • return count(): int,
  • return default(): MariaDbInstance,
  • return find(string $instanceId): MariaDbInstance,
  • validate user-submitted instance ids.

Create exec/lib/MySQLServiceFactory.php.

Responsibilities:

  • receive Settings, shared credentials, and registry,
  • create a MySQLService for a selected instance,
  • create services for all instances,
  • expose fleet-level helpers such as databaseExistsAnywhere() and listDatabasesForUserAcrossInstances().

Existing Class Changes

Modify exec/bootstrap.php:

  • load shared credentials once from alt-mysql.conf,
  • load instance registry,
  • construct MySQLServiceFactory,
  • construct default MySQLService for old handlers that are still single-instance during transition,
  • pass registry and factory into AppContext.

Modify exec/lib/AppContext.php:

  • add public MariaDbInstanceRegistry $instances,
  • add public MySQLServiceFactory $mysqlFactory,
  • keep public MySQLService $mysql as the default instance for backward compatibility during the refactor.

Modify exec/lib/MySQLService.php:

  • accept a MariaDbInstance in the constructor,
  • use selected instance socket/port in connection credentials,
  • add instanceId(): string,
  • make metadata schema instance-aware,
  • scope owner/grant/host/sso metadata queries by instance_id.

PHP Tests

Add:

  • tests/instance_registry_test.php
  • tests/mysql_service_factory_test.php
  • tests/multi_instance_metadata_schema_test.php

Test cases:

  • registry loads two instances from fixture env files,
  • legacy env migrates to registry model,
  • invalid instance id is rejected,
  • factory builds credentials with shared user/password but per-instance socket/port,
  • ensureMetadataSchema() creates instance-aware metadata definitions,
  • databaseExistsAnywhere() checks every instance.

Shell Runtime Refactor

scripts/setup/mysql_common.sh

Refactor the shell helper so every script can select an instance.

New contract:

MYSQL_ALT_INSTANCE_ID="${MYSQL_ALT_INSTANCE_ID:-}"
mysql_load_alt_runtime "$MYSQL_ALT_INSTANCE_ID"

If no instance id is provided:

  • use ALT_MARIADB_DEFAULT_INSTANCE,
  • if only one instance exists, use that instance,
  • if more than one instance exists and no default is configured, fail with a clear message.

New functions:

  • mysql_registry_file
  • mysql_instance_dir
  • mysql_list_instances
  • mysql_default_instance
  • mysql_validate_instance_id
  • mysql_load_instance_env
  • mysql_instance_socket "$id"
  • mysql_instance_port "$id"
  • mysql_instance_basedir "$id"
  • mysql_instance_service "$id"
  • mysql_base_args_for_instance "$id"

Keep existing wrappers like mysql_alt_socket and mysql_alt_port, but make them call the selected runtime.

Shell Tests

Update:

  • tests/mysql_common_runtime_test.sh
  • tests/mysql_host_sync_test.sh
  • tests/backup_queue_service_test.sh
  • tests/restore_rollback_safety_test.sh

Add:

  • tests/mysql_common_multi_runtime_test.sh

Test cases:

  • loading a selected instance returns the selected socket/port,
  • loading without instance id uses default when only one instance exists,
  • loading without instance id fails when multiple instances exist and no default is set,
  • binary detection prefers the selected instance basedir,
  • generated mysql arguments include shared credentials plus selected socket.

UI And User Workflow

Main Page

Modify exec/handlers/index.php.

When registry count is 1:

  • keep the current layout,
  • keep showing the single server version.

When registry count is greater than 1:

  • do not show a single global MariaDB version in the header,
  • add a table column Wersja MariaDB,
  • show each database row with its instance version,
  • include hidden instance_id fields in row actions: phpMyAdmin login, backup, manage privileges, delete.

Create Database Form

When registry count is greater than 1:

  • show a required select field Wersja MariaDB,
  • options come from the registry, sorted by version,
  • default option is ALT_MARIADB_DEFAULT_INSTANCE,
  • create the database through the selected instance service.

When registry count is 1:

  • do not show the selector,
  • create in the only/default instance.

Validation:

  • reject unknown instance id,
  • reject creating a database name that already exists in any plugin-managed instance,
  • in advanced mode, list existing users only from the selected instance.

Manage, Delete, Privileges, phpMyAdmin

Every handler that references a database must accept instance_id:

  • index.php
  • modify_privileges.php
  • assign_database.php
  • change_password.php
  • delete_databases.php
  • delete_users.php
  • manage_hosts.php
  • open_phpmyadmin.php
  • upload_backup.php

For GET links, include instance_id in the query. For POST forms, include it as hidden input and validate it through MariaDbInstanceRegistry.

UI Tests

Extend PHP tests or add handler-focused fixtures:

  • one instance: no version selector, no version column, global version visible,
  • two instances: version selector visible, version column visible, global version hidden,
  • create database posts selected instance_id,
  • row actions include selected instance_id.

phpMyAdmin Integration

Current private phpMyAdmin SSO assumes one endpoint. Refactor ticket payloads to include instance routing.

Ticket payload should include:

{
  "version": 2,
  "instance_id": "11.4",
  "db": "user_db",
  "auth": {
    "host": "localhost",
    "port": 3020,
    "socket": "/var/lib/alt-mariadb-11.4/mariadb.sock",
    "user": "da_tmp_phpmyadmin_x",
    "password": "..."
  }
}

The generated phpMyAdmin da_login.php must:

  • read the ticket,
  • use the ticket socket/port instead of a single default alt-my.cnf endpoint,
  • still use the shared credential file only for bootstrap/admin operations,
  • reject tickets with unknown instance_id.

PhpMyAdminSso must:

  • find the requested database and instance,
  • create temporary SSO users in that selected instance only,
  • write the selected instance id into the ticket.

Tests:

  • opening phpMyAdmin for a database on 11.4 writes a ticket with instance_id=11.4,
  • generated signon script uses ticket endpoint,
  • unknown instance id fails.

Backup And Restore

Queue Jobs

Every job file must include:

INSTANCE_ID='11.4'
DB_NAME='user_db'

Lock files must include instance id:

data/lock/11.4__user_db.lock

This prevents a backup on 10.11/user_db from blocking or being confused with 11.4/user_db if duplicate database names are later allowed. Even while global uniqueness is enforced, this makes the queue future-proof.

Backup Job

Modify BackupQueueService::queueBackupJob():

  • accept instance_id,
  • validate that the database exists in that instance,
  • write INSTANCE_ID into the job env.

Modify scripts/backup_job.sh:

  • read INSTANCE_ID,
  • call mysql_load_alt_runtime "$INSTANCE_ID",
  • log the selected instance id and version,
  • write backup sidecar metadata next to the SQL dump.

Sidecar example:

{
  "source": "alt-mysql",
  "instance_id": "11.4",
  "mariadb_version": "11.4",
  "database": "user_db",
  "created_at": "2026-06-16T20:00:00+02:00"
}

Restore Job

Modify restore queue methods:

  • accept target instance_id,
  • validate target database in that instance,
  • write INSTANCE_ID into job env.

Modify scripts/restore_job.sh:

  • read INSTANCE_ID,
  • call mysql_load_alt_runtime "$INSTANCE_ID",
  • perform rollback backup in the selected instance,
  • restore only into the selected instance.

For external backups without plugin sidecar metadata:

  • the restore form must require the target database, and if more than one instance exists it must also require the target MariaDB version,
  • no payload conversion is required,
  • restore uses the selected target instance.

For local backups created by the plugin:

  • default restore target should be the source instance from sidecar metadata,
  • allow restoring into another version only when ALLOW_RESTORE_TO_OTHER_DATABASE=true and the target database already exists or the workflow explicitly creates it.

DirectAdmin Backup/Restore Hooks

Modify:

  • scripts/da-integration/alt_mysql_user_backup_compress_pre.sh
  • scripts/da-integration/alt_mysql_restore_payload.sh
  • scripts/da-integration/da_restore_native_staging_to_alt_mysql.sh

Backup hook:

  • iterate every installed instance,
  • dump user databases from each instance,
  • write manifest entries with instance_id, mariadb_version, socket, port, database, sha256, and relative file path,
  • dump metadata tables per instance.

Restore hook with plugin payload:

  • read instance_id from manifest,
  • if that instance exists, restore there,
  • if it does not exist, use a configurable fallback:
    • default: fail with a clear message,
    • optional setting: restore to ALT_MARIADB_DEFAULT_INSTANCE.

Restore hook for external/native backups without plugin payload:

  • continue the staging flow through native DirectAdmin MariaDB,
  • migrate staged databases to ALT_MARIADB_DEFAULT_INSTANCE by default,
  • add setting DA_ALT_MYSQL_EXTERNAL_RESTORE_INSTANCE=default where value can be a concrete instance id such as 11.4,
  • preserve database name, user name, and password as currently required.

Tests:

  • backup hook manifest contains two databases on two versions,
  • payload restore uses manifest-selected instance,
  • external restore without payload uses configured fallback instance,
  • restore fails clearly when manifest references a missing instance and no fallback is enabled.

Remote Hosts And CSF

mysql_host_sync.sh currently syncs one port and one config. Refactor it to iterate all active instances:

  • for every installed instance, update bind-address according to allow_remote_hosts,
  • restart only services whose config changed,
  • write CSF allow rules for every active instance port and every allowed remote host,
  • keep using a separate managed file such as /etc/csf/alt-mysql-plugin.allow,
  • include comments that identify instance id and port.

Example managed CSF line:

tcp|in|d=3020|s=203.0.113.10 # alt-mysql 11.4 user_db_user

Tests:

  • one host and two instances produce two CSF entries,
  • disabling remote hosts removes managed entries for every instance,
  • config rewrite changes all instance configs from 127.0.0.1 to configured bind address.

Upgrade Workflow

scripts/setup/mysql_upgrade.sh must become instance-aware.

Required CLI:

TARGET_MARIADB_VERSION=11.8 SOURCE_INSTANCE_ID=11.4 ./scripts/setup/mysql_upgrade.sh

Behavior:

  • load source instance from registry,
  • install target binaries to /usr/local/mariadb-11.8,
  • keep the same datadir only when performing an in-place major upgrade,
  • if target version implies new instance id, update registry atomically,
  • maintain rollback of binaries and config,
  • run mariadb-upgrade against the selected socket,
  • update per-instance env file and registry only after health checks pass.

Recommended first implementation:

  • support patch-level upgrades within the same instance id first,
  • support cross-series upgrade only as a separate, explicit task after multi-instance installation is stable.

Uninstall And Health Checks

Modify scripts/uninstall.sh:

  • support uninstalling one instance by id,
  • refuse to remove shared credential files while any instance remains,
  • remove shared credential files only when the last instance is removed and the user explicitly confirms.

Modify scripts/setup/alt_mysql_health_check.sh:

  • with no args, check every registered instance,
  • with INSTANCE_ID=11.4, check only that instance,
  • validate service active, socket exists, shared credential login works, metadata schema exists.

Add tests:

  • health check iterates two fixtures,
  • uninstall of one instance leaves shared credentials and other instance registry entry,
  • uninstall of last instance removes registry only when explicit cleanup flag is set.

Implementation Tasks

Task 1: Add Registry Fixtures And Failing Tests

Files:

  • Create: tests/fixtures/legacy-alt-mariadb.env
  • Create: tests/fixtures/alt-mariadb.env
  • Create: tests/fixtures/alt-mariadb.d/10.11.env
  • Create: tests/fixtures/alt-mariadb.d/11.4.env
  • Create: tests/instance_registry_test.php
  • Create: tests/mysql_common_multi_runtime_test.sh

Steps:

  • Add fixture env files matching the formats in this plan.
  • Write instance_registry_test.php to assert two instances load and legacy migration is detected.
  • Write mysql_common_multi_runtime_test.sh to assert selected instance socket/port.
  • Run the tests and confirm they fail because registry classes/functions do not exist.

Expected red failures:

  • Class "MariaDbInstanceRegistry" not found
  • mysql_list_instances: command not found

Task 2: Implement PHP Registry

Files:

  • Create: exec/lib/MariaDbInstance.php
  • Create: exec/lib/MariaDbInstanceRegistry.php
  • Modify: exec/bootstrap.php
  • Modify: tests/instance_registry_test.php

Steps:

  • Implement strict env parsing with only expected keys.
  • Reject instance ids outside ^[0-9]+\\.[0-9]+$.
  • Load index file and per-instance env files.
  • Implement legacy single-instance detection and migration helper.
  • Add bootstrap autoload entries.
  • Run php tests/instance_registry_test.php and confirm pass.
  • Run php -l exec/lib/MariaDbInstance.php exec/lib/MariaDbInstanceRegistry.php.

Task 3: Implement Shell Registry Runtime

Files:

  • Modify: scripts/setup/mysql_common.sh
  • Test: tests/mysql_common_multi_runtime_test.sh
  • Test: tests/mysql_common_runtime_test.sh

Steps:

  • Add registry path variables.
  • Add instance list/default/validation functions.
  • Refactor mysql_load_alt_runtime to accept an optional instance id.
  • Keep old wrappers for compatibility.
  • Run shell tests and bash -n scripts/setup/mysql_common.sh.

Task 4: Refactor Installer For Per-Version Instances

Files:

  • Modify: scripts/install_db.sh
  • Create: tests/install_db_multi_instance_test.sh
  • Create: tests/registry_migration_test.sh

Steps:

  • Change derived paths to /var/lib/alt-mariadb-$version, /etc/alt-mariadb-$version.cnf, and alt-mariadb-$version.
  • Add first-install credential initialization.
  • Add later-install credential reuse from existing registry.
  • Add registry writer and per-instance env writer.
  • Add legacy registry migration.
  • Add non-interactive secure-install SQL function.
  • Add tests for path derivation, credential reuse, registry update, and secure SQL markers.
  • Run installer tests and bash -n scripts/install_db.sh.

Task 5: Make MySQLService Instance-Aware

Files:

  • Modify: exec/lib/MySQLService.php
  • Create: exec/lib/MySQLServiceFactory.php
  • Modify: exec/bootstrap.php
  • Modify: exec/lib/AppContext.php
  • Create: tests/mysql_service_factory_test.php
  • Create: tests/multi_instance_metadata_schema_test.php

Steps:

  • Add MariaDbInstance constructor argument to MySQLService.
  • Scope metadata schema by instance_id.
  • Add factory methods for selected and all instances.
  • Add fleet-level database uniqueness check.
  • Update bootstrap and context.
  • Run PHP tests and php -l for touched files.

Task 6: Refactor Main UI And Create Database Flow

Files:

  • Modify: exec/handlers/index.php
  • Modify: exec/handlers/create_database.php
  • Modify: user/enhanced.css
  • Modify: user/evolution.css
  • Add/modify handler tests according to current test style.

Steps:

  • Aggregate database rows across all instances.
  • Hide global MariaDB version when more than one instance exists.
  • Add version column only when more than one instance exists.
  • Add version selector to create form only when more than one instance exists.
  • Validate selected instance id on create.
  • Enforce global database-name uniqueness across all instances.
  • Keep one-instance UI unchanged.

Task 7: Pass Instance Id Through All Database Actions

Files:

  • Modify: exec/handlers/modify_privileges.php
  • Modify: exec/handlers/assign_database.php
  • Modify: exec/handlers/change_password.php
  • Modify: exec/handlers/delete_databases.php
  • Modify: exec/handlers/delete_users.php
  • Modify: exec/handlers/manage_hosts.php
  • Modify: exec/handlers/open_phpmyadmin.php
  • Modify: exec/handlers/upload_backup.php

Steps:

  • Add hidden instance_id to every POST form.
  • Add instance_id query param to every database action URL.
  • Validate every submitted instance id through registry.
  • Use selected MySQLService from factory.
  • Reject actions where the selected database does not exist in the selected instance.

Task 8: Refactor phpMyAdmin SSO

Files:

  • Modify: exec/lib/PhpMyAdminSso.php
  • Modify: scripts/setup/phpmyadmin_install.sh
  • Modify: scripts/setup/phpmyadmin_private_install.sh
  • Modify: scripts/setup/phpmyadmin_health_check.sh
  • Modify: tests/phpmyadmin_install_test.sh

Steps:

  • Add instance_id to SSO ticket payload.
  • Include selected socket/port in ticket auth data.
  • Make da_login.php use ticket endpoint, not a singleton socket from shared alt-my.cnf.
  • Reject unknown instance ids.
  • Test signon against fixture ticket for 11.4.

Task 9: Refactor Backup And Restore Queue

Files:

  • Modify: exec/lib/BackupQueueService.php
  • Modify: scripts/backup_job.sh
  • Modify: scripts/restore_job.sh
  • Modify: scripts/worker.sh
  • Modify: tests/backup_queue_service_test.sh
  • Modify: tests/restore_rollback_safety_test.sh

Steps:

  • Add INSTANCE_ID to job env files.
  • Include INSTANCE_ID in lock paths.
  • Load selected runtime in backup and restore jobs.
  • Write backup sidecar metadata with instance id/version.
  • Default local restore to source instance when sidecar exists.
  • Require selected target instance for external uploads when more than one instance exists.

Task 10: Refactor DirectAdmin Backup/Restore Hooks

Files:

  • Modify: scripts/da-integration/alt_mysql_user_backup_compress_pre.sh
  • Modify: scripts/da-integration/alt_mysql_restore_payload.sh
  • Modify: scripts/da-integration/da_restore_native_staging_to_alt_mysql.sh
  • Modify: scripts/setup/da_integration_install.sh
  • Add/modify DA integration tests.

Steps:

  • Iterate all installed instances during backup.
  • Add instance metadata to payload manifest.
  • Restore payload entries to their source instance when present.
  • Add setting DA_ALT_MYSQL_EXTERNAL_RESTORE_INSTANCE.
  • Restore external backups without payload to configured/default instance.
  • Preserve database name, user name, and password.

Task 11: Refactor Remote Host Sync And CSF

Files:

  • Modify: scripts/setup/mysql_host_sync.sh
  • Modify: plugin-settings.conf
  • Modify: tests/mysql_host_sync_test.sh

Steps:

  • Iterate active instances.
  • Rewrite bind-address per instance config.
  • Generate managed CSF allow entries for every instance port and allowed host.
  • Restart only changed services.
  • Test two ports with one remote host.

Task 12: Refactor Upgrade, Health Check, And Uninstall

Files:

  • Modify: scripts/setup/mysql_upgrade.sh
  • Modify: scripts/setup/alt_mysql_health_check.sh
  • Modify: scripts/uninstall.sh
  • Modify: tests/mysql_upgrade_rollback_test.sh

Steps:

  • Add instance selection to upgrade.
  • Keep rollback per selected instance.
  • Run mariadb-upgrade through selected socket.
  • Health-check every instance by default.
  • Uninstall one instance without removing shared credentials.

Task 13: Packaging And Versioning

Files:

  • Modify: plugin.conf
  • Modify: tests/plugin_identity_test.sh
  • Modify: tests/package_archive_test.sh

Steps:

  • Bump plugin version for the implementation release.
  • Ensure package excludes tests and macOS metadata.
  • Ensure archive contains new PHP classes and no stale single-instance installer names.
  • Run the full validation command.
  • Build alt-mysql.tar.gz without the parent directory.

Full Validation Command

Run after implementation:

set -euo pipefail
find scripts -type f -name '*.sh' -print0 | sort -z | xargs -0 -n1 bash -n
find exec -type f -name '*.php' -print0 | sort -z | xargs -0 -n1 php -l
bash tests/plugin_identity_test.sh
php tests/instance_registry_test.php
php tests/mysql_service_factory_test.php
php tests/multi_instance_metadata_schema_test.php
bash tests/mysql_common_runtime_test.sh
bash tests/mysql_common_multi_runtime_test.sh
bash tests/install_db_cli_test.sh
bash tests/install_db_multi_instance_test.sh
bash tests/registry_migration_test.sh
php tests/always_enabled_test.php
bash tests/custom_package_items_test.sh
bash tests/da_integration_install_test.sh
bash tests/custombuild_hooks_install_test.sh
bash tests/roundcube_repair_config_test.sh
bash tests/mysql_host_sync_test.sh
php tests/settings_remote_hosts_test.php
bash tests/phpmyadmin_install_test.sh
bash tests/restore_rollback_safety_test.sh
bash tests/mysql_upgrade_rollback_test.sh
bash tests/backup_queue_service_test.sh
bash tests/package_archive_test.sh

Rollout Strategy

  1. Release registry support while still operating one instance.
  2. Migrate existing single-instance installs into the registry format.
  3. Install a second instance on a staging VM with a distinct port.
  4. Verify UI database creation on both versions.
  5. Verify phpMyAdmin SSO per version.
  6. Verify plugin backup/restore per version.
  7. Verify DirectAdmin backup/restore payload with two versions.
  8. Verify external DirectAdmin restore without payload goes to configured default instance.
  9. Enable remote hosts and verify CSF rules for every active instance port.

Production Acceptance Criteria

  • Installing ./scripts/install_db.sh 10.11 3011 creates /var/lib/alt-mariadb-10.11, /var/lib/alt-mariadb-10.11/mariadb.sock, and alt-mariadb-10.11.service.
  • Installing ./scripts/install_db.sh 11.4 3020 does not overwrite the 10.11 registry, datadir, service, socket, config, or credentials.
  • /usr/local/directadmin/conf/alt-mysql.conf and /usr/local/directadmin/conf/alt-my.cnf are shared and contain the same admin/root secret for all instances.
  • Every installed instance has had anonymous users removed, test database removed, remote root accounts removed, and local root/admin password set.
  • With one instance, the UI remains close to the current UI and shows the server version.
  • With more than one instance, the UI shows a database-version column, shows a version selector during database creation, and does not show one global server version.
  • Backup, restore, delete, privilege editing, host management, and phpMyAdmin login all use the selected instance_id.
  • External DirectAdmin restores without plugin payload restore to the configured/default alt instance and preserve database name, user name, and password.
  • CSF rules open every active alt MariaDB port only for the hosts defined in the plugin.