diff --git a/server/datastore/mysql/migrations/tables/20210819131107_AddCascadeToHostSoftware.go b/server/datastore/mysql/migrations/tables/20210819131107_AddCascadeToHostSoftware.go index 0f5c422919..f1721cf57b 100644 --- a/server/datastore/mysql/migrations/tables/20210819131107_AddCascadeToHostSoftware.go +++ b/server/datastore/mysql/migrations/tables/20210819131107_AddCascadeToHostSoftware.go @@ -30,6 +30,16 @@ func Up_20210819131107(tx *sql.Tx) error { } } + // Clear any orphan software and host_software + _, err = tx.Exec(`DELETE FROM host_software WHERE NOT EXISTS (select 1 from hosts h where h.id=host_software.host_id)`) + if err != nil { + return errors.Wrap(err, "clearing orphan host_software") + } + _, err = tx.Exec(`DELETE FROM software WHERE NOT EXISTS (select 1 from host_software hs where hs.software_id=software.id)`) + if err != nil { + return errors.Wrap(err, "clearing orphan software") + } + if _, err := tx.Exec(` ALTER TABLE host_software ADD FOREIGN KEY host_software_hosts_fk(host_id) REFERENCES hosts (id) ON DELETE CASCADE, diff --git a/server/datastore/mysql/mysql_test.go b/server/datastore/mysql/mysql_test.go index b582686f3b..d64477f8e6 100644 --- a/server/datastore/mysql/mysql_test.go +++ b/server/datastore/mysql/mysql_test.go @@ -9,13 +9,16 @@ import ( "path" "runtime" "testing" + "time" "github.com/DATA-DOG/go-sqlmock" "github.com/VividCortex/mysqlerr" "github.com/WatchBeam/clock" "github.com/fleetdm/fleet/v4/server/config" + "github.com/fleetdm/fleet/v4/server/datastore/mysql/migrations/tables" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/test" "github.com/go-kit/kit/log" "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" @@ -510,3 +513,72 @@ func TestMigrations(t *testing.T) { base := path.Dir(filename) require.NoError(t, ioutil.WriteFile(path.Join(base, "schema.sql"), stdoutBuf.Bytes(), 0o655)) } + +func Test20210819131107_AddCascadeToHostSoftware(t *testing.T) { + // Create the database (must use raw MySQL client to do this) + db, err := sql.Open( + "mysql", + fmt.Sprintf("%s:%s@tcp(%s)/?multiStatements=true", testUsername, testPassword, testAddress), + ) + require.NoError(t, err) + defer db.Close() + _, err = db.Exec("DROP DATABASE IF EXISTS Test20210819131107_AddCascadeToHostSoftware; CREATE DATABASE Test20210819131107_AddCascadeToHostSoftware;") + require.NoError(t, err) + + // Create a datastore client in order to run migrations as usual + config := config.MysqlConfig{ + Username: testUsername, + Password: testPassword, + Address: testAddress, + Database: "Test20210819131107_AddCascadeToHostSoftware", + } + ds, err := New(config, clock.NewMockClock(), Logger(log.NewNopLogger()), LimitAttempts(1)) + require.NoError(t, err) + defer ds.Close() + + for { + version, err := tables.MigrationClient.GetDBVersion(ds.db.DB) + require.NoError(t, err) + + // break right before the the constraint migration + if version == 20210818182258 { + break + } + require.NoError(t, tables.MigrationClient.UpByOne(ds.db.DB, "")) + } + + host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now()) + host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now()) + + soft1 := fleet.HostSoftware{ + Modified: true, + Software: []fleet.Software{ + {Name: "foo", Version: "0.0.1", Source: "chrome_extensions"}, + {Name: "foo", Version: "0.0.3", Source: "chrome_extensions"}, + }, + } + host1.HostSoftware = soft1 + soft2 := fleet.HostSoftware{ + Modified: true, + Software: []fleet.Software{ + {Name: "foo", Version: "0.0.2", Source: "chrome_extensions"}, + {Name: "foo", Version: "0.0.3", Source: "chrome_extensions"}, + {Name: "bar", Version: "0.0.3", Source: "deb_packages"}, + }, + } + host2.HostSoftware = soft2 + host2.Modified = true + + require.NoError(t, ds.SaveHostSoftware(host1)) + require.NoError(t, ds.SaveHostSoftware(host2)) + + require.NoError(t, ds.DeleteHost(host1.ID)) + + require.NoError(t, tables.MigrationClient.UpByOne(ds.db.DB, "")) + + // Make sure we don't delete more than we need + hostCheck, err := ds.Host(host2.ID) + require.NoError(t, err) + require.NoError(t, ds.LoadHostSoftware(hostCheck)) + require.Len(t, hostCheck.Software, 3) +}