Skip to content

Known-open defects after v0.16.0 — 0.16.1 scoping #60

Description

@chlunde

Tracking list of known-open defects after v0.16.0, so 0.16.1 can be scoped deliberately rather than by whoever shouts loudest.

Read this first: almost nothing here should hold up 0.16.1. One item (A1) is a genuine 0.16.0 regression with a one-line fix and is the only thing I'd argue for blocking on. A2 is a second small regression worth taking if it's cheap. Everything in the "pre-existing" tables has been broken for one or more releases, so shipping 0.16.1 without it regresses nobody.

Verified against upstream/master @ fe39828 (2026-08-11). Detailed write-ups with proofs live in POTENTIAL_1.16_ISSUES.md (item numbers below match it); the release triage view is in the remediation-plan artifact.

Every CRD below exists twice — cluster-scoped (*.sql.crossplane.io) and namespaced (*.sql.m.crossplane.io). Fixes go in one PR covering both trees, per CLAUDE.md.


A. 0.16.1 candidates (regressions introduced in 0.16.0)

A1 — Deleting a Role's connection Secret rotates the live database password · P1 · Role (postgresql)

Items 54 and 55 in POTENTIAL_1.16_ISSUES.md; standalone write-up in ISSUE_348_password_rotation.md. Introduced by crossplane-contrib#348 (be0bb83), shipped in v0.16.0.

Create never stamps Status.AtProvider.LastPasswordChange — the only stamp is in Update (pkg/controller/cluster/postgresql/role/reconciler.go:359). So for every provider-created role that field stays nil forever, and shouldResetPassword (role/utils.go:76) branches on exactly that:

last := role.Status.AtProvider.LastPasswordChange
if last != nil {
    if role.Spec.ForProvider.PasswordRotationTrigger != nil {
        return role.Spec.ForProvider.PasswordRotationTrigger.After(last.Time), nil  // item 55: unreachable
    }
    return false, nil
}
// last == nil -> the connection Secret decides:
return true, nil                                        // item 54: Secret absent -> rotate
return len(s.Data[...PasswordKey]) == 0, nil            // item 54: empty password -> rotate

Two symptoms from one asymmetry:

  • 54 — a GitOps prune / kubectl delete secret / namespace cleanup makes the next reconcile run ALTER ROLE ... PASSWORD ..., and consumers holding the old password stop authenticating.
  • 55passwordRotationTrigger, the documented way to rotate, is never read for a normally-created role, so it silently does nothing.

Honest bounds (this is why it's P1 and not "drop everything"): only affects roles with a provider-generated password (passwordSecretRef unset) and writeConnectionSecretToRef set; fires only when the Secret is actually missing or has an empty password key; self-limiting, because the first reset stamps the field. BYOP roles never reach the heuristic.

Fix: stamp LastPasswordChange in Create right after the successful CREATE ROLE, both trees. One line each, and it repairs 54 and 55 together. Note master already ships TestGetPassword/NilLastPasswordChangeSecretNotFound asserting changed: true for this exact input — that test encodes the bug and has to change with the fix.

Reproduce (unit, no database): a Role with WriteConnectionSecretToReference set and LastPasswordChange == nil, test.MockClient whose Get returns NotFound; assert getPassword returns changed == false. Fails on master. For item 55: same role with a healthy Secret and PasswordRotationTrigger an hour in the future; assert changed == true. Both were run in a throwaway worktree at fe39828 and failed as expected.

Related, and worth gating: PR crossplane-contrib#424 ports this design to MySQL User with the same two defects (LastPasswordChange stamped only in UpdatePassword, missing Secret ⇒ true). Ask for Create to stamp the field before merging, or it lands the same bug in a second engine.

A2 — Legacy objectType: schema resources can't be deleted after crossplane-contrib#379 · P1 · DefaultPrivileges (postgresql)

Item 53. Introduced by crossplane-contrib#379, shipped in v0.16.0.

crossplane-contrib#379 made schema optional and added guards to Observe (default_privileges/reconciler.go:255,259). The second guard rejects objectType: schema with a schema set — which is every such resource created on 0.14/0.15, because schema was +required then.

Because an Observe error returns before the deletion block in crossplane-runtime's managed reconciler (managed/reconciler.go:1118 vs :1165), those resources can't be deleted: the finalizer is never removed and kubectl delete hangs. CEL doesn't help — XValidation runs on writes and never re-validates stored objects.

Fix (~5 lines/tree): when validation fails but meta.WasDeleted(mg) is true, return ResourceExists: false instead of an error. Create always failed for these, so there's no external state to revoke. Needs a regression test with a deletion timestamp in both trees.

Alternative if 0.16.1 needs to be small: ship a release note telling upgraders to remove spec.schema from objectType: schema resources, and fix it in 0.17.


B. Also new in 0.16.0, but not release-blocking

Item CRD Pri Summary
52 User (mysql) P2 authenticationPlugin.authString: "" never converges — Observe maps '' to nil, authPluginEqual (mysql/user/reconciler.go:296) reads that as drift, ALTER USER ... IDENTIFIED WITH re-fires every reconcile. Needs a deliberately empty string. ~2 lines: normalise ""nil, or add MinLength=1.
56 User (mysql) P1* Adding authenticationPlugin: {name: caching_sha2_password} to a User that already has a password makes Update emit ALTER USER ... IDENTIFIED WITH <plugin> with no AS clause (:494-506). *The provider half is proven; the engine half is not — nobody has confirmed MySQL actually clears the credential in that case. Run it against mysql:8 before treating it as P1; if the credential survives, this folds into item 52.
57 User (mysql) P3 user_types.go:82 tells you to use passwordSecretRef with a native password plugin; the CEL rule at :38 makes those two mutually exclusive. Doc-only.
58 User (mysql) P3 Not a new defect — crossplane-contrib#363 added call sites of the QuoteValue helper from item 12 (below), including the user-controlled authString. Just means item 12's fix has more callers than it used to.

C. Pre-existing — none of this regresses 0.16.x

Broken for at least one release. High user pain in places, but shipping 0.16.1 without them changes nothing for anyone.

PostgreSQL

Item CRD Pri Summary
3, 46, 51 DefaultPrivileges P1 Observe filters on neither defaclnamespace nor defaclrole, so resources differing only by schema or target role alias onto each other and never converge; ALL is never expanded on the desired side; Create's REVOKE ALL erases sibling CRs' privileges. Broken since 0.14. One query rewrite; 51 needs a semantics decision first.
16, 17, 18 Grant P2 Observe demands the ACL equal the spec exactly, while Create only revokes the spec's own privileges. Consequences: two Grants on one object fight forever (18); a grant to the object's owner never converges (17); a drifted Grant reports ResourceExists: false, so deleting it skips the REVOKE and leaves the privilege behind (16). crossplane-contrib#421 fixed the database-level case; tables/sequences/schemas still exact-match — see selectTableGrantQuery's array_agg(...) = .... Containment vs. equality is a design decision — state it in the PR.
4, 6, 48, 50 Role P1 upToDate (role/reconciler.go:437) compares pointers, not values, so every role reports out-of-date from reconcile #2 and Update re-issues ALTER ROLE ... CONNECTION LIMIT forever (issue crossplane-contrib#194). Also: Create and Update quote configurationParameters differently and Observe can't parse what PostgreSQL stores back (6); out-of-band rolconfig drift is detected but never written (48); Create omits CONNECTION LIMIT (50).
41, 42 Database P2 isTemplate: true makes the CR undeletable — Delete issues a bare DROP DATABASE (database/reconciler.go:311) and PostgreSQL refuses. upToDate also diffs four axes Update can't write, so non-canonical encoding spellings loop forever; owner: DEFAULT, which the field's own doc recommends, is a hard create failure.
43 Extension P1 spec.forProvider.schema is a no-op end to end — not emitted by Create, not read by Observe, Update is empty. Version drift is diffed but never written. Decide: implement it or remove it (removing later is breaking).
44 Schema P2 revokePublicOnSchema is write-only — never observed, so out-of-band re-grants to PUBLIC are invisible drift on a security knob. Same class as the already-fixed item 2; commit 93b607b's acl.grantee = 0 clause is the template.
31, 45 clients / all PG CRDs P1 DSN() (pkg/clients/postgresql/postgresql.go:50) embeds the password in a URL; when it fails to parse, lib/pq echoes the whole DSN — password included — into status.conditions (issue crossplane-contrib#266). Same function: database is concatenated without url.PathEscape, so a crafted name can override sslmode/host. Pre-existing, so not a 0.16.1 blocker, but it's cheap and it's a credential leak.

MySQL

Item CRD Pri Summary
30, 7, 8 Grant P1 MySQL collapses a complete privilege list to ALL PRIVILEGES; diffPermissions compares that against the user's explicit list and issues REVOKE ALL + re-GRANT every reconcile — a real window with no privileges (issue crossplane-contrib#162). Also: REVOKE ... WITH GRANT OPTION (mysql/grant/reconciler.go:364,380) is invalid MySQL, so deleting such a grant wedges the finalizer. The e2e suite runs MariaDB, which accepts that syntax — that's why it never caught it.
5 User P1 Same pointer comparison as item 4 (mysql/user/reconciler.go:566): any User with resourceOptions never reports up-to-date.
9 Database P1 defaultCollation is late-initialised from the server into the spec; a later charset-only change then emits an incompatible COLLATE and Update fails forever.

MSSQL

Item CRD Pri Summary
19, 20, 21 User P1 Contained-user support shipped in 0.15 with zero tests. The immutability CEL rule has an unset→set hole, so contained: true can be patched onto an existing login-mapped user; Delete then drops the user but not the login, leaving a working server credential behind. The namespaced tree emits USE [db];, which Azure SQL rejects, and database isn't required, so the cluster tree silently creates the user in master.
11, 12 Grant, clients P1/P3 mssql.QuoteIdentifier (pkg/clients/mssql/mssql.go:130) doesn't escape ], and the grant path interpolates the schema name raw (mssql/grant/reconciler.go:258). Mostly a correctness bug: a schema named My-Schema simply doesn't work.
23 Grant P2 Update revokes every database-class permission not in the spec, so two Grants on one user fight, and any Grant omitting CONNECT locks the user out. Pre-existing since 2021 — document the one-Grant-per-user constraint, or adopt whatever containment semantics items 16–18 land on.

Cross-cutting

Item Affects Pri Summary
35 every CRD P2 Connection details are returned from Update only when the password changed, so a ProviderConfig endpoint change never reaches the managed resource's Secret (crossplane-contrib#242) and adopted roles never publish a username (crossplane-contrib#77). Touches every reconciler — schedule it alone, it conflicts with everything.
22 ClusterProviderConfig (postgresql, mssql) P2 Only the MySQL namespaced config package registers a reconciler for its ClusterProviderConfig, so the PG and MSSQL ones get no finalizer and no status.users — delete one while in use and it goes immediately, breaking every dependent MR.
25 provider startup P3 Every Setup registers a state-metrics recorder whose Start returns the List error, which kingpin.FatalIfError turns into process exit — a CRD not established ~5s after start crash-loops the provider instead of leaving a metric unpopulated. ~5 lines: log instead of return, plus a nil guard on MetricOptions.
27 namespaced CRDs P3 Marker defects from the tree split: namespaced MySQL Grant doesn't require forProvider, namespaced MSSQL User lost its categories, namespaced MySQL ProviderConfig TLS refs are cross-namespace, and 4 of 6 ProviderConfig CRDs have a stale SECRET-NAME printcolumn. Includes a real nil-deref: Observe panics on a Grant with no user in both trees.
49 all PG CRDs P2 No adoption or ownership policy: two CRs pointing at one external object produce owner ping-pong (Database, Schema), silently stale credentials (Role), or destructive CASCADE deletes. Needs one documented answer, not six per-resource behaviours.

Working on any of these

  • Both trees, one PR. pkg/controller/cluster/... and pkg/controller/namespaced/..., plus apis/cluster and apis/namespaced. Diff the two diffs against each other before review.
  • Detail is in POTENTIAL_1.16_ISSUES.md — each item there has the proof, the exact SQL or Go, the ruled-out hypotheses, and a fix direction. Read the item before starting; several have a "this looked like a bug and isn't" note that will save a round.
  • Probes get promoted, not deleted. Any throwaway test that demonstrates one of these should land as a regression test with the fix.
  • Observe must issue zero writes on reconcile chore: bump korthout/backport-action from 1.4.0 to 3.2.0 #2. Most items above are that invariant failing.
  • Unit tests use go-sqlmock / table-driven go-cmp; make reviewable before pushing; e2e needs Docker and only one run at a time (the KIND cluster name is shared).

Already fixed — don't re-do these

#1 routine-grant argument quoting, #15 grants on views/partitioned tables, #47a Connect on backends without server_version_num, #47b database grants no longer needing a session on the target DB, #2 revokePublicOnDb, #17 for databases (tables/sequences/schemas still open, see above) — all in crossplane-contrib#421. #32 objectType: schema SQL in crossplane-contrib#379. #34 shared login in crossplane-contrib#411. #36 x/net CVE in crossplane-contrib#407. #14/#37 CodeQL version mismatch via the action bump.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions