[management] Fold casing on agent network identity the legacy schema kept

The legacy bootstrap stored the cluster as the caller spelled it — trimmed,
never folded — and the reshape that shipped in 0.78 copied it into
proxy_address, and subdomain.cluster into domain, verbatim. Everything that
reads those columns compares against canonical lowercase: proxies
canonicalise their address at connect, the gateway-pin check a proxy
registration runs matches proxy_address exactly, cluster-scoped synthesis
finds an account's row by proxy_address, and the proxy folds the SNI host
before matching a mapping's domain. A row that kept capitals is invisible
to all of them — its pin protects nothing and its endpoint never routes.

The reshape now writes LOWER() for both columns, and an idempotent
normaliser lowercases rows a released reshape already copied, registered
right after it. The predicate selects only rows that would change, so a
normalised table costs one pass over one row per account; on MySQL the
default collation compares case-insensitively already and it is a no-op.

Reported by cubic on #7402: the exact proxy_address match a proxy
registration relies on misses a migrated pin with capitals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
This commit is contained in:
mlsmaycon
2026-09-12 12:53:15 +00:00
co-authored by Claude Fable 5.1
parent 138cb3b3e0
commit 39f8ea3f70
2 changed files with 111 additions and 6 deletions
+49 -3
View File
@@ -757,8 +757,9 @@ func TestMigrateAgentNetworkSettingsToDomain_BackfillsAndDropsLegacyColumns(t *t
db := setupDatabase(t)
require.NoError(t, db.Migrator().DropTable(&legacyAgentNetworkSettings{}))
require.NoError(t, db.AutoMigrate(&legacyAgentNetworkSettings{}))
// Spelled the way the legacy bootstrap kept it: trimmed, never folded.
require.NoError(t, db.Create(&legacyAgentNetworkSettings{
AccountID: "acct-1", Cluster: "eu.proxy.netbird.io", Subdomain: "violet", EnableLogCollection: true,
AccountID: "acct-1", Cluster: "EU.Proxy.NetBird.io", Subdomain: "Violet", EnableLogCollection: true,
}).Error)
require.NoError(t, db.Create(&legacyAgentNetworkSettings{
AccountID: "acct-2", Cluster: "us.proxy.netbird.io", Subdomain: "violet",
@@ -770,8 +771,10 @@ func TestMigrateAgentNetworkSettingsToDomain_BackfillsAndDropsLegacyColumns(t *t
var one, two agentNetworkTypes.Settings
require.NoError(t, db.First(&one, "account_id = ?", "acct-1").Error)
assert.Equal(t, "violet.eu.proxy.netbird.io", one.Domain, "domain must combine subdomain and cluster")
assert.Equal(t, "eu.proxy.netbird.io", one.ProxyAddress, "proxy address must carry the cluster")
assert.Equal(t, "violet.eu.proxy.netbird.io", one.Domain,
"domain must combine subdomain and cluster, folded to the canonical lowercase every reader compares against")
assert.Equal(t, "eu.proxy.netbird.io", one.ProxyAddress,
"proxy address must carry the cluster in canonical lowercase, matching what proxies register under")
assert.True(t, one.EnableLogCollection, "non-identity fields must ride through")
require.NoError(t, db.First(&two, "account_id = ?", "acct-2").Error)
assert.Equal(t, "violet.us.proxy.netbird.io", two.Domain,
@@ -858,3 +861,46 @@ func TestMigrateAgentNetworkSettingsToDomain_ResumesAfterPartialDrop(t *testing.
assert.Equal(t, "violet.eu.proxy.netbird.io", row.Domain, "migrated values must be untouched")
assert.Equal(t, "eu.proxy.netbird.io", row.ProxyAddress, "migrated values must be untouched")
}
// TestNormalizeAgentNetworkSettingsIdentity_LowercasesReshapedRows covers rows
// a released reshape already copied verbatim: capitals kept from the legacy
// cluster spelling are folded in place, canonical rows are left alone, and
// non-identity fields ride through.
func TestNormalizeAgentNetworkSettingsIdentity_LowercasesReshapedRows(t *testing.T) {
ctx := context.Background()
db := setupDatabase(t)
require.NoError(t, db.Migrator().DropTable(&agentNetworkTypes.Settings{}))
require.NoError(t, db.AutoMigrate(&agentNetworkTypes.Settings{}))
require.NoError(t, db.Create(&agentNetworkTypes.Settings{
AccountID: "acct-legacy", Domain: "Violet.EU.Proxy.NetBird.io", ProxyAddress: "EU.Proxy.NetBird.io", EnableLogCollection: true,
}).Error)
require.NoError(t, db.Create(&agentNetworkTypes.Settings{
AccountID: "acct-canonical", Domain: "amber.us.proxy.netbird.io", ProxyAddress: "us.proxy.netbird.io",
}).Error)
require.NoError(t, migration.NormalizeAgentNetworkSettingsIdentity(ctx, db))
var legacy, canonical agentNetworkTypes.Settings
require.NoError(t, db.First(&legacy, "account_id = ?", "acct-legacy").Error)
assert.Equal(t, "violet.eu.proxy.netbird.io", legacy.Domain, "a mixed-case endpoint must be folded where it is stored")
assert.Equal(t, "eu.proxy.netbird.io", legacy.ProxyAddress, "a mixed-case pin must be folded so exact lookups find it")
assert.True(t, legacy.EnableLogCollection, "non-identity fields must ride through")
require.NoError(t, db.First(&canonical, "account_id = ?", "acct-canonical").Error)
assert.Equal(t, "amber.us.proxy.netbird.io", canonical.Domain, "a canonical row must be left as it is")
assert.Equal(t, "us.proxy.netbird.io", canonical.ProxyAddress)
require.NoError(t, migration.NormalizeAgentNetworkSettingsIdentity(ctx, db),
"a second run over a normalised table must be a no-op, not an error")
}
// TestNormalizeAgentNetworkSettingsIdentity_SkipsMissingTable pins that a
// store which never had agent network settings is left untouched.
func TestNormalizeAgentNetworkSettingsIdentity_SkipsMissingTable(t *testing.T) {
ctx := context.Background()
db := setupDatabase(t)
require.NoError(t, db.Migrator().DropTable(&agentNetworkTypes.Settings{}))
require.NoError(t, migration.NormalizeAgentNetworkSettingsIdentity(ctx, db),
"no table must be a no-op, not an error")
assert.False(t, db.Migrator().HasTable(&agentNetworkTypes.Settings{}), "the normaliser must not create the table")
}