diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 6815241311fbe..055e972b7f550 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -2792,6 +2792,7 @@ jobs: - py3.13-8.0.18 - py3.13-8.0.36 - py3.13-8.4.0 + - py3.13-9 - py3.13-8.0-group - py3.13-8.0-hybrid - py3.13-mariadb-10.5 diff --git a/mysql/hatch.toml b/mysql/hatch.toml index 8cb380b080d6b..31abb6f0b9981 100644 --- a/mysql/hatch.toml +++ b/mysql/hatch.toml @@ -22,6 +22,7 @@ version = [ "8.0.18", # Test for pre-json aggregation version "8.0.36", # EOL April, 2026 "8.4.0", + "9", # latest 9.x innovation release ] [[envs.default.matrix]] @@ -56,6 +57,9 @@ version = [ matrix.version.env-vars = [ { key = "COMPOSE_FILE", value = "mysql8.yaml", if = ["8.0.18", "8.0.36"] }, { key = "COMPOSE_FILE", value = "mysql8.yaml", if = ["8.4.0"] }, + # MySQL 9 uses the official `mysql` image (bitnamilegacy is deprecated), which needs + # its own compose with initdb-based replication. + { key = "COMPOSE_FILE", value = "mysql-official.yaml", if = ["9"] }, ] name."8.0-group".env-vars = [ { key = "COMPOSE_FILE", value = "mysql8-group.yaml" }, diff --git a/mysql/tests/common.py b/mysql/tests/common.py index f6efed6c7b258..44f4af84b6af7 100644 --- a/mysql/tests/common.py +++ b/mysql/tests/common.py @@ -25,6 +25,20 @@ # adding flavor to differentiate mariadb from mysql MYSQL_FLAVOR = os.getenv('MYSQL_FLAVOR', '') + +def mysql_root_password(): + """Root password for the current image. The official mysql (MySQL 9) and mariadb (12.1+/13 RC) + images and percona set one, as do the group/hybrid replication composes; the bitnami images used + for older versions (and the ``latest`` tag) allow an empty root password.""" + if MYSQL_FLAVOR == 'percona' or MYSQL_REPLICATION in ('group', 'hybrid'): + return 'mypass' + if MYSQL_FLAVOR == 'mysql' and os.getenv('MYSQL_VERSION', '').startswith('9'): + return 'mypass' + if MYSQL_FLAVOR == 'mariadb' and MYSQL_VERSION_PARSED > parse_version('12.0'): + return 'mypass' + return None + + HOST = get_docker_hostname() PORT = 13306 SLAVE_PORT = 13307 diff --git a/mysql/tests/compose/mysql-official-primary.conf b/mysql/tests/compose/mysql-official-primary.conf new file mode 100644 index 0000000000000..1f0d51bb0e1de --- /dev/null +++ b/mysql/tests/compose/mysql-official-primary.conf @@ -0,0 +1,23 @@ +[mysqld] +server-id = 1 +log_bin = /var/lib/mysql/mysql-bin.log +binlog_format = ROW +enforce_gtid_consistency = ON +gtid_mode = ON + +# Activate general and slow query logs, and locate them under a shared folder so we +# can share them with the Agent container. Path must match _mysql_logs_path (/var/log/mysql). +general_log = on +general_log_file = /var/log/mysql/mysql.log +slow_query_log = on +slow_query_log_file = /var/log/mysql/mysql_slow.log +long_query_time = 0.1 + +performance_schema = ON +performance-schema-consumer-events-statements-current = ON +performance-schema-consumer-events-statements-history = ON +performance-schema-consumer-events-statements-history-long = ON +performance-schema-consumer-events-waits-current = ON +max_digest_length = 4096 +performance_schema_max_digest_length = 4096 +performance_schema_max_sql_text_length = 4096 diff --git a/mysql/tests/compose/mysql-official-replica.conf b/mysql/tests/compose/mysql-official-replica.conf new file mode 100644 index 0000000000000..a8a053fe1584a --- /dev/null +++ b/mysql/tests/compose/mysql-official-replica.conf @@ -0,0 +1,22 @@ +[mysqld] +server-id = 2 +relay-log = mysql-relay-bin +enforce_gtid_consistency = ON +gtid_mode = ON + +# Activate general and slow query logs, and locate them under a shared folder so we +# can share them with the Agent container. Path must match _mysql_logs_path (/var/log/mysql). +general_log = on +general_log_file = /var/log/mysql/mysql.log +slow_query_log = on +slow_query_log_file = /var/log/mysql/mysql_slow.log +long_query_time = 0.1 + +performance_schema = ON +performance-schema-consumer-events-statements-current = ON +performance-schema-consumer-events-statements-history = ON +performance-schema-consumer-events-statements-history-long = ON +performance-schema-consumer-events-waits-current = ON +max_digest_length = 4096 +performance_schema_max_digest_length = 4096 +performance_schema_max_sql_text_length = 4096 diff --git a/mysql/tests/compose/mysql-official.yaml b/mysql/tests/compose/mysql-official.yaml new file mode 100644 index 0000000000000..cb2855fb96986 --- /dev/null +++ b/mysql/tests/compose/mysql-official.yaml @@ -0,0 +1,43 @@ +# Official `mysql` image (Docker Hub library), used for MySQL 9 instead of the +# deprecated bitnamilegacy/mysql image. Modeled on percona.yaml: replication is +# bootstrapped via /docker-entrypoint-initdb.d scripts and per-role config files. +# +# NOTE (flagged for CI validation): this compose has not been run in CI yet — the +# healthcheck and GTID replication bootstrap for the official mysql 9 image need to be +# confirmed on a real run. See DBMON-6806. +services: + mysql-master: + image: "${MYSQL_DOCKER_REPO}:${MYSQL_IMAGE_TAG}" + environment: + - MYSQL_ROOT_PASSWORD=mypass + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pmypass"] + interval: 5s + timeout: 5s + retries: 10 + ports: + - "${MYSQL_PORT}:3306" + volumes: + - ${MYSQL_LOGS_HOST_PATH}:${MYSQL_LOGS_PATH} + - ./mysql-official-primary.conf:/etc/mysql/conf.d/primary.cnf + - ./mysql_official_primary_initdb:/docker-entrypoint-initdb.d + + mysql-slave: + container_name: mysql-slave + image: "${MYSQL_DOCKER_REPO}:${MYSQL_IMAGE_TAG}" + environment: + - MYSQL_ROOT_PASSWORD=mypass + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pmypass"] + interval: 5s + timeout: 5s + retries: 10 + ports: + - "${MYSQL_SLAVE_PORT}:3306" + volumes: + - ${MYSQL_LOGS_HOST_PATH}:${MYSQL_LOGS_PATH} + - ./mysql-official-replica.conf:/etc/mysql/conf.d/replica.cnf + - ./mysql_official_replica_initdb:/docker-entrypoint-initdb.d + depends_on: + mysql-master: + condition: service_healthy diff --git a/mysql/tests/compose/mysql_official_primary_initdb/01_replication.sql b/mysql/tests/compose/mysql_official_primary_initdb/01_replication.sql new file mode 100644 index 0000000000000..efb0770d9537c --- /dev/null +++ b/mysql/tests/compose/mysql_official_primary_initdb/01_replication.sql @@ -0,0 +1,3 @@ +CREATE USER 'replica_user'@'%' IDENTIFIED BY 'replica_password'; +GRANT REPLICATION SLAVE ON *.* TO 'replica_user'@'%'; +FLUSH PRIVILEGES; diff --git a/mysql/tests/compose/mysql_official_replica_initdb/01_replication.sql b/mysql/tests/compose/mysql_official_replica_initdb/01_replication.sql new file mode 100644 index 0000000000000..0030a63a9e2b9 --- /dev/null +++ b/mysql/tests/compose/mysql_official_replica_initdb/01_replication.sql @@ -0,0 +1,2 @@ +CHANGE REPLICATION SOURCE TO SOURCE_HOST='mysql-master',SOURCE_USER='replica_user',SOURCE_PASSWORD='replica_password',GET_SOURCE_PUBLIC_KEY=1; +START REPLICA; diff --git a/mysql/tests/conftest.py b/mysql/tests/conftest.py index 7d6fbf657f93b..dc07f17e4b960 100644 --- a/mysql/tests/conftest.py +++ b/mysql/tests/conftest.py @@ -22,6 +22,8 @@ MYSQL_FLAVOR = os.getenv('MYSQL_FLAVOR') MYSQL_VERSION = os.getenv('MYSQL_VERSION') COMPOSE_FILE = os.getenv('COMPOSE_FILE') +# Explicit image tag override (e.g. a prerelease "13.0-rc"); falls back to the version. +MYSQL_IMAGE_TAG = os.getenv('MYSQL_IMAGE_TAG') or MYSQL_VERSION @pytest.fixture(scope='session') @@ -53,8 +55,13 @@ def dd_environment(config_e2e): logs_path = _mysql_logs_path() with TempDir('logs') as logs_host_path: - # for Ubuntu - os.chmod(logs_host_path, 0o770) + # for Ubuntu, make the shared logs dir writable by the container's user. In E2E runs + # TempDir reuses the same host dir across env start/test; when it is no longer owned by + # the current user the re-chmod is not permitted, but it was already set on creation. + try: + os.chmod(logs_host_path, 0o770) + except PermissionError: + pass e2e_metadata = {'docker_volumes': ['{}:{}'.format(logs_host_path, logs_path)]} @@ -62,6 +69,7 @@ def dd_environment(config_e2e): os.path.join(common.HERE, 'compose', COMPOSE_FILE), env_vars={ 'MYSQL_DOCKER_REPO': _mysql_docker_repo(), + 'MYSQL_IMAGE_TAG': MYSQL_IMAGE_TAG, 'MYSQL_PORT': str(common.PORT), 'MYSQL_SLAVE_PORT': str(common.SLAVE_PORT), 'MYSQL_CONF_PATH': _mysql_conf_path(), @@ -306,7 +314,8 @@ def instance_hybrid_traditional_replica(): def version_metadata(): parts = MYSQL_VERSION.split('-') version = parts[0].split('.') - major, minor = version[:2] + major = version[0] + minor = version[1] if len(version) > 1 else mock.ANY patch = version[2] if len(version) > 2 else mock.ANY if MYSQL_FLAVOR == 'percona': @@ -490,9 +499,7 @@ def _create_enable_consumers_procedure(conn): def init_master(): logger.debug("initializing master") - conn = pymysql.connect( - host=common.HOST, port=common.PORT, user='root', password='mypass' if MYSQL_FLAVOR == 'percona' else None - ) + conn = pymysql.connect(host=common.HOST, port=common.PORT, user='root', password=common.mysql_root_password()) _add_dog_user(conn) _add_bob_user(conn) _add_fred_user(conn) @@ -505,7 +512,7 @@ def _get_root_connection(): host=common.HOST, port=common.PORT, user='root', - password='mypass' if MYSQL_FLAVOR == 'percona' or MYSQL_REPLICATION in ('group', 'hybrid') else None, + password=common.mysql_root_password(), ) @@ -580,7 +587,7 @@ def populate_database(): host=common.HOST, port=common.PORT, user='root', - password='mypass' if MYSQL_REPLICATION in ('group', 'hybrid') or MYSQL_FLAVOR == 'percona' else None, + password=common.mysql_root_password(), ) cur = conn.cursor() @@ -739,6 +746,10 @@ def _mysql_docker_repo(): return 'bergerx/mysql-replication' elif MYSQL_VERSION.startswith('8') or MYSQL_VERSION == 'latest': return 'bitnamilegacy/mysql' + elif MYSQL_VERSION.startswith('9'): + # bitnamilegacy/mysql is deprecated; use the official `mysql` image for 9+ + # (see compose/mysql-official.yaml). + return 'mysql' elif MYSQL_FLAVOR == 'mariadb': return 'bitnamilegacy/mariadb' elif MYSQL_FLAVOR == 'percona': diff --git a/mysql/tests/test_query_activity.py b/mysql/tests/test_query_activity.py index b0cb19330e23d..7c92432b1a1b9 100644 --- a/mysql/tests/test_query_activity.py +++ b/mysql/tests/test_query_activity.py @@ -22,7 +22,7 @@ from datadog_checks.mysql.activity import MySQLActivity from datadog_checks.mysql.util import StatementTruncationState -from .common import CHECK_NAME, HOST, MYSQL_FLAVOR, MYSQL_REPLICATION, MYSQL_VERSION_PARSED, PORT +from .common import CHECK_NAME, HOST, MYSQL_FLAVOR, MYSQL_REPLICATION, MYSQL_VERSION_PARSED, PORT, mysql_root_password ACTIVITY_JSON_PLANS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "activity") @@ -707,7 +707,7 @@ def test_mdl_blocking_activity(aggregator, dbm_instance, dd_run_check, root_conn # Commit to release it before we set up the intentional MDL contention. root_conn.commit() - root_password = 'mypass' if MYSQL_FLAVOR == 'percona' or MYSQL_REPLICATION in ('group', 'hybrid') else None + root_password = mysql_root_password() holder_conn = pymysql.connect(host=HOST, port=PORT, user='root', password=root_password) ddl_conn = pymysql.connect(host=HOST, port=PORT, user='root', password=root_password) ddl_ready = Event()