fleet/server/datastore/mysql/migrations_test.go
Jordan Montgomery 662f3f5e4d
Fix for 4.73.2 misnumbered migrations (#33655)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #33562 

Detects and if possible fixes migrations which were misnumbered in the
released 4.73.2 Linux binary(it was based on the commit before the
renumbering commit was added). This does not affect the released 4.73.2
docker images and this code does nothing on these since the migrations
will not be detected

We specifically look for the 3 most recent migrations being the
mis-numbered 4.73.2 and 4.73.1 migrations in the expected order. If
neither of the mis-numbered migrations are found, nothing is done.
Likewise if the order is not right or the order is not exactly
right(e.g. if intervening migrations, for instance from 4.74.0 have been
applied) we do not apply the fix. Finally, the fix is only ever applied
in the existing migration path and fleet will never try to apply the
fleet automatically by just running the fleet server(though it will
detect the condition and complain)

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [x] Confirmed that the fix is not expected to adversely impact load
test results
- [x] Alerted the release DRI if additional load testing is needed

## Database migrations

- [x] Checked table schema to confirm autoupdate
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2025-09-30 16:01:17 -05:00

178 lines
6.6 KiB
Go

package mysql
import (
"context"
"os/exec"
"testing"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql/testing_utils"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/migrations/data"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/migrations/tables"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMigrationStatus(t *testing.T) {
ds := createMySQLDSForMigrationTests(t, t.Name())
t.Cleanup(func() {
ds.Close()
})
status, err := ds.MigrationStatus(context.Background())
require.NoError(t, err)
assert.EqualValues(t, fleet.NoMigrationsCompleted, status.StatusCode)
assert.Empty(t, status.MissingTable)
assert.Empty(t, status.MissingData)
require.Nil(t, ds.MigrateTables(context.Background()))
status, err = ds.MigrationStatus(context.Background())
require.NoError(t, err)
assert.EqualValues(t, fleet.SomeMigrationsCompleted, status.StatusCode)
assert.NotEmpty(t, status.MissingData)
require.Nil(t, ds.MigrateData(context.Background()))
status, err = ds.MigrationStatus(context.Background())
require.NoError(t, err)
assert.EqualValues(t, fleet.AllMigrationsCompleted, status.StatusCode)
assert.Empty(t, status.MissingTable)
assert.Empty(t, status.MissingData)
// Insert unknown migration.
_, err = ds.writer(context.Background()).Exec(`INSERT INTO ` + tables.MigrationClient.TableName + ` (version_id, is_applied) VALUES (1638994765, 1)`)
require.NoError(t, err)
status, err = ds.MigrationStatus(context.Background())
require.NoError(t, err)
assert.EqualValues(t, fleet.UnknownMigrations, status.StatusCode)
_, err = ds.writer(context.Background()).Exec(`DELETE FROM ` + tables.MigrationClient.TableName + ` WHERE version_id = 1638994765`)
require.NoError(t, err)
status, err = ds.MigrationStatus(context.Background())
require.NoError(t, err)
assert.EqualValues(t, fleet.AllMigrationsCompleted, status.StatusCode)
assert.Empty(t, status.MissingTable)
assert.Empty(t, status.MissingData)
}
func TestV4732MigrationFix(t *testing.T) {
ds := createMySQLDSForMigrationTests(t, t.Name())
t.Cleanup(func() {
ds.Close()
})
status, err := ds.MigrationStatus(context.Background())
require.NoError(t, err)
require.NotNil(t, status)
assert.EqualValues(t, fleet.NoMigrationsCompleted, status.StatusCode)
recreate4732BadState(t, ds)
status, err = ds.MigrationStatus(context.Background())
require.NoError(t, err)
require.NotNil(t, status)
assert.EqualValues(t, fleet.NeedsFleetv4732Fix, status.StatusCode)
err = ds.FixFleetv4732Migrations(context.Background())
require.NoError(t, err)
err = ds.MigrateTables(context.Background())
require.NoError(t, err)
status, err = ds.MigrationStatus(context.Background())
require.NoError(t, err)
require.NotNil(t, status)
assert.EqualValues(t, fleet.AllMigrationsCompleted, status.StatusCode)
// Insert a bad migration again which should trigger the unknown state
_, err = ds.writer(context.Background()).Exec(`INSERT INTO `+tables.MigrationClient.TableName+` (version_id, is_applied) VALUES (?, 1)`, fleet4732BadMigrationID1)
require.NoError(t, err)
status, err = ds.MigrationStatus(context.Background())
require.NoError(t, err)
require.NotNil(t, status)
assert.EqualValues(t, fleet.UnknownFleetv4732State, status.StatusCode)
}
// Apply the proper 4.73.2 migrations
func recreate4732GoodState(t *testing.T, ds *Datastore) {
var version int64
var err error
const maxDataMigration = 20230525175650
// Migrate up to 4.73.1
for version < fleet4731GoodMigrationID {
err = tables.MigrationClient.UpByOne(ds.writer(context.Background()).DB, "")
require.NoError(t, err)
version, err = tables.MigrationClient.GetDBVersion(ds.writer(context.Background()).DB)
require.NoError(t, err)
}
require.Equal(t, int64(fleet4731GoodMigrationID), version)
// Apply data migrations which were deprecated before 4.73.2 and should never change so no need for
// upbyone, etc. but we'll verify below that we're at expected version
err = data.MigrationClient.Up(ds.writer(context.Background()).DB, "")
require.NoError(t, err)
version, err = data.MigrationClient.GetDBVersion(ds.writer(context.Background()).DB)
require.NoError(t, err)
require.EqualValues(t, int64(maxDataMigration), version)
// Apply the migrations from fleet v4.73.2
err = tables.MigrationClient.UpByOne(ds.writer(context.Background()).DB, "")
require.NoError(t, err)
version, err = tables.MigrationClient.GetDBVersion(ds.writer(context.Background()).DB)
require.NoError(t, err)
require.EqualValues(t, fleet4732GoodMigrationID2, version)
err = tables.MigrationClient.UpByOne(ds.writer(context.Background()).DB, "")
require.NoError(t, err)
version, err = tables.MigrationClient.GetDBVersion(ds.writer(context.Background()).DB)
require.NoError(t, err)
require.EqualValues(t, fleet4732GoodMigrationID1, version)
}
// Recreate the bad state that some customers ended up with after running fleet v4.73.2 migrations
func recreate4732BadState(t *testing.T, ds *Datastore) {
recreate4732GoodState(t, ds)
_, err := ds.writer(context.Background()).Exec(`UPDATE `+tables.MigrationClient.TableName+` SET version_id = ? WHERE version_id = ?`, fleet4732BadMigrationID1, fleet4732GoodMigrationID1)
require.NoError(t, err)
_, err = ds.writer(context.Background()).Exec(`UPDATE `+tables.MigrationClient.TableName+` SET version_id = ? WHERE version_id = ?`, fleet4732BadMigrationID2, fleet4732GoodMigrationID2)
require.NoError(t, err)
version, err := tables.MigrationClient.GetDBVersion(ds.writer(context.Background()).DB)
require.NoError(t, err)
require.EqualValues(t, fleet4732BadMigrationID1, version)
}
func TestMigrations(t *testing.T) {
// Create the database (must use raw MySQL client to do this)
ds := createMySQLDSForMigrationTests(t, t.Name())
defer ds.Close()
require.NoError(t, ds.MigrateTables(context.Background()))
// Dump schema to dumpfile
cmd := exec.Command( // nolint:gosec // Waive G204 since this is a test file
"docker", "compose", "exec", "-T", "mysql_test",
// Command run inside container
"mysqldump", "-u"+testing_utils.TestUsername, "-p"+testing_utils.TestPassword, "TestMigrations", "--compact", "--skip-comments",
)
output, err := cmd.CombinedOutput()
require.NoError(t, err, "mysqldump: %s", string(output))
}
func createMySQLDSForMigrationTests(t *testing.T, dbName string) *Datastore {
// Create a datastore client in order to run migrations as usual
config := config.MysqlConfig{
Username: testing_utils.TestUsername,
Password: testing_utils.TestPassword,
Address: testing_utils.TestAddress,
Database: dbName,
}
ds, err := newDSWithConfig(t, dbName, config)
require.NoError(t, err)
return ds
}