# 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` ```ini host= user=da_admin passwd= database=mysql ``` `/usr/local/directadmin/conf/alt-my.cnf` ```ini [client] user="da_admin" password="" ``` 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: ```bash # 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/.env`. Example: ```bash 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/.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: ```sql 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: ```bash ./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: ```sql 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/.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: ```bash 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: ```json { "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: ```bash INSTANCE_ID='11.4' DB_NAME='user_db' ``` Lock files must include instance id: ```text 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: ```json { "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: ```text 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: ```bash 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: ```bash 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.