Repository navigation
MySQL and MariaDB driver - #9
Merged
Merged
Conversation
A driver that creates one backend principal per access (the mysql driver's database users) needs a stable, unique name for it, and GrantRequest carried only the Buckety side. The access's namespace and name are both immutable for its lifetime and unique together, so they are what such a driver derives the principal from; drivers with shared principals ignore the new fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Buckety is a database (CREATE DATABASE IF NOT EXISTS, optional characterSet/collation), a BucketyAccess is a user of its own with the role's privileges on that one database: Reader gets SELECT, ReadWrite the data changes plus the DDL schema migrations need. Writer is refused rather than given a guessed grant set. Delete drops the database; users are dropped with their access under every retention policy. Security model. The controller's account needs CREATE USER on *.*, which neither server can scope by name and which also allows ALTER/RENAME/DROP USER on every account. The backend's namePrefix is therefore the boundary, enforced in one place (checkManaged): every database and user name, including principals read back from status, must start with it, be lowercase [a-z0-9_-] within the limits, and not be a system name; the prefix must end in "_", stay off system names, and exclude the admin account. Grants use the database name with LIKE wildcards escaped (an unescaped "_" in b_t1_orders would also match b_t1xorders). Account names and passwords travel as ? arguments with client-side interpolation, because CREATE USER/GRANT are not parameterisable server-side; identifiers are validated and backtick-quoted; privilege names read from the server are checked before they reach REVOKE. Names. Databases are the resolved spec.name, which must carry the prefix, so status.backendResourceName and the Secret's database key are the real name. Users are <prefix><namespace>_<name>, or a hashed form when that does not fit MySQL 8's 32-character limit (MariaDB allows 80; one scheme keeps backends portable). Kubernetes names have no "_", so the two forms cannot collide. Drift without reading the mysql schema. The admin account cannot see other accounts or their grants, so the driver logs in as each user with the Secret's password: success reads the user's own SCHEMA_PRIVILEGES and aligns grants (revoking extras and GRANT OPTION), writing nothing when nothing differs, which matters on Galera where account changes replicate as TOI. A refused login means a missing user or a changed password: CREATE or ALTER USER, grant, and verify with a short retry for Galera lag. Passwords are generated from crypto/rand at the first grant and read back from the Secret afterwards, so a password written into the Secret is applied and deleting the Secret rotates it. Errors from statements carrying a password are redacted; no DSN strings are built. Tested with the env-gated integration test against MariaDB 10.11.14 and MySQL 8.0.46 run from distro packages in userspace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
examples/mysql/bootstrap creates the controller's account where the instance is created: an initdb component adds buckety-account.sh to /docker-entrypoint-initdb.d and mounts the Secret it reads the password from, so no password exists in source. The image entrypoint runs it once, on the first boot of an empty data directory, with root on the local socket - the only way in when root may connect only from localhost, as in kubernetes-mysql-cluster. The component targets that project's StatefulSet mariadb, so any variant takes it; the example pins variants/scale-1. The script runs in a subshell so the entrypoint may source or execute it, validates the password with the driver's rule, passes it on stdin, and is idempotent for manual runs against existing instances. e2e: a single ephemeral MariaDB, shaped like kubernetes-mysql-cluster so the same component applies, with the password generated by mariadb-secret.sh into both namespaces that need it (a pod mounts Secrets only from its own namespace). The controller env is optional so clusters without it still start. Scenarios: happy-path (database with charset, ReadWrite user running DDL from a Job) and multi-consumer (Reader denied INSERT/CREATE by the server, Writer refused with GrantFailed and no Secret, deletion blocked by accesses). Both are unrun: no k3d/docker where they were written. secret_has_keys printed the whole Secret on a missing key; it now prints key names only, so a failing run does not put credentials in CI logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPEC gains the driver in the driver table, the Secret output section with the prefix boundary as a contract-level property, the database notion of empty for adoption, the CI matrix row, and the scoping exception in the non-goals. README covers the backend config and the CREATE USER caveat for cluster maintainers. docs/mysql.md carries the detail: the account's grants, why CREATE USER makes the prefix the security boundary and that sensitive accounts belong on an instance without a backend, naming rules, roles, drift detection by login, SQL construction, MySQL 8 notes, and the design decisions, listed for review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- userHost must be lowercase. Servers store hosts lowercased, so an uppercase userHost made every login check read as shadowed (an ALTER USER and grants on every pass) and a case-only change would have revoked the live account as the "old" principal. - An error from CREATE/ALTER USER keeps only the server's error number, with a hint for the policy and privilege cases. A parse error's message quotes the statement and can cut it mid-password, which whole-string redaction cannot catch. - The cleanup DROP USER after a failed pass runs detached from the request context, which may be what failed. - A line break in adminPassword is a startup error, and the bootstrap script refuses a password file with a trailing newline: the controller gets the Secret verbatim through env, while $(cat) stripped the newline, so the two would never match. - The e2e secret copy uses create/replace instead of apply, which would also store the data in last-applied-configuration. - docs/mysql.md: cleartext passwords without tls, grants the login check cannot see, the fixed namePrefix and how to retire one, trust in status within the prefix, ReadWrite's DROP on its own database, Secret-edit churn on Galera, SYSTEM_USER on MySQL 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MySQL 8.4 refuses REVOKE of a privilege the account does not hold (error 1141), and a REVOKE naming several privileges is atomic. The first grant of a Reader revoked the rest of the ReadWrite set in one statement, so on 8.4 a Reader access could never be granted; MariaDB and MySQL 8.0 accept such a REVOKE silently. A user created in the same pass holds nothing, so the first grant now revokes nothing. A re-keyed user (password from a deleted or edited Secret) still has unknown grants: the rest of the managed set is revoked one privilege at a time, with "no such grant" as the expected answer for each one it does not hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gcs test comment pointed at a requirements document in a private repository; it now says what the case is. Two docs/mysql.md paragraphs are reflowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test/integration/mysql.sh runs TestIntegration against MariaDB 10.11 and 11.4 and MySQL 8.0 and 8.4, one container each. The controller account is created the way a cluster creates it: the bootstrap script from examples/mysql runs from /docker-entrypoint-initdb.d as root on the socket, root only from localhost, so the script is tested against every image too. The e2e workflow runs the same script as its own job, and publish waits for it. BUCKETY_MYSQL_TEST_ALT_USER_HOST=auto takes the address the server sees the test connect from, so the userHost-change step runs there as well. A new step rotates a Reader's password by deleting its Secret, the re-keyed path whose grants are unknown; on MySQL 8.4 that path needs one REVOKE per privilege. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two findings from a second review, of how the driver meets the reconcile loops: - Changing an existing access's role to Writer was refused before anything reached the server, so the access reported GrantFailed while its user kept ReadWrite's grants and its Secret kept working. A role without a grant set now drops the access's user (DROP USER IF EXISTS, one statement per failing reconcile). Changing the role back recreates the user with the password still in the Secret. - DROP DATABASE waits for open transactions on the database, and consumer sessions outlive DROP USER. The server's default wait is a day (MariaDB) or a year (MySQL), while the client gives up after 60s, so every retry queued another DROP whose pending lock blocks new queries on that database. The controller's sessions now set lock_wait_timeout to 50s, below the client timeout. The integration test covers both against every server image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- an access changed to Writer loses its user; - user names are unique within a cluster, not across clusters: one controller per server and namePrefix; - a generated password is verified on the next re-check, not in the pass that sets it, and the Galera retry is about four seconds; - grant changes reach an open session when it next selects the database; DROP DATABASE gives up after lock_wait_timeout; - more ways a user ends up recorded by no access, and that a shadowing account costs replicated statements on every re-check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ParameterDrift pauses re-checks until the Buckety changes, by the controller's design for drift that needs a human. The mysql message told users to ALTER DATABASE out of band, which alone leaves the Buckety at Ready=False indefinitely; e2e found it. The message and docs/mysql.md now say to annotate the Buckety afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mysql had happy-path and multi-consumer only (both now run on a local k3d against the MariaDB backing service). Added: - the shared bodies: backend-stickiness, driver-version, misconfigured-startup (plus an adminUser inside namePrefix) and scaled-to-zero; - oob-drift: DROP USER, extra grants with GRANT OPTION, DROP DATABASE and a changed character set, each made as root; password rotation by deleting the Secret, and a password written into it; - adoption: AdoptEmpty refuses a database with a table, adopts an empty one, Adopt claims the first, and adopted databases survive retentionPolicy=Delete; - retention-policy: Retain keeps the rows and drops the user, Delete drops both; - parameter-mutation: immutable characterSet/collation, unknown keys, utf8, a name outside namePrefix, access parameters. lib.sh gains mysql_root (root on the pod's socket), mysql_login (a Secret's credentials over TCP, password on stdin) and wait_until. backend_stickiness_scenario deletes sticky-new while its backend is still registered, and accepts the block showing on the implicit access: an access holding a principal of its own (a mysql user) cannot be revoked once its backend is gone, and left the namespace stuck terminating. kadm, s3 and gcs pass unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
go.mod requires go 1.27.0 and names toolchain go1.27.1 explicitly (a toolchain line equal to the go line is dropped by go mod tidy). CI's setup-go reads go-version-file: go.mod and installs the toolchain directive's version, so CI rebuilds the pinned image with the same compiler as scripts/bump-release.sh, which refuses any other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docker.io/minio/minio no longer serves anonymous pulls, so the backing service never became ready and CI failed before any scenario ran. ghcr.io/yolean/minio mirrors the releases; its newest is RELEASE.2025-07-18T21-56-31Z, pinned by digest like the other backing-service images. All s3 scenarios pass against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.