cloud/bmdb: add up/down migration test

This isn't very exhaustive, but it's enough to catch migration issues
which we already had.

Change-Id: Ie26b7646bb8b051a613e75cb69a1708f9288a0cc
Reviewed-on: https://review.monogon.dev/c/monogon/+/1137
Tested-by: Jenkins CI
Reviewed-by: Leopold Schabel <leo@monogon.tech>
diff --git a/cloud/bmaas/bmdb/BUILD.bazel b/cloud/bmaas/bmdb/BUILD.bazel
index 8c67b9c..d735b3f 100644
--- a/cloud/bmaas/bmdb/BUILD.bazel
+++ b/cloud/bmaas/bmdb/BUILD.bazel
@@ -23,6 +23,7 @@
 go_test(
     name = "bmdb_test",
     srcs = [
+        "migrations_test.go",
         "queries_test.go",
         "reflection_test.go",
         "sessions_test.go",
diff --git a/cloud/bmaas/bmdb/migrations_test.go b/cloud/bmaas/bmdb/migrations_test.go
new file mode 100644
index 0000000..24d17a4
--- /dev/null
+++ b/cloud/bmaas/bmdb/migrations_test.go
@@ -0,0 +1,40 @@
+package bmdb
+
+import (
+	"testing"
+)
+
+// TestMigrateUpDown performs a full-up and full-down migration test on an
+// in-memory database twice.
+//
+// Doing this the first time allows us to check the up migrations are valid and
+// that the down migrations clean up enough after themselves for earlier down
+// migrations to success.
+//
+// Doing this the second time allows us to make sure the down migrations cleaned
+// up enough after themselves that they have left no table/type behind.
+func TestMigrateUpDown(t *testing.T) {
+	// Start with an empty database.
+	b := dut()
+	_, err := b.Open(false)
+	if err != nil {
+		t.Fatalf("Starting empty database failed: %v", err)
+	}
+
+	// Migrations go up.
+	if err := b.Database.MigrateUp(); err != nil {
+		t.Fatalf("Initial up migration failed: %v", err)
+	}
+	// Migrations go down.
+	if err := b.Database.MigrateDownDangerDanger(); err != nil {
+		t.Fatalf("Initial down migration failed: %v", err)
+	}
+	// Migrations go up.
+	if err := b.Database.MigrateUp(); err != nil {
+		t.Fatalf("Second up migration failed: %v", err)
+	}
+	// Migrations go down.
+	if err := b.Database.MigrateDownDangerDanger(); err != nil {
+		t.Fatalf("Second down migration failed: %v", err)
+	}
+}
diff --git a/cloud/bmaas/bmdb/model/migrations/1662136250_initial.down.sql b/cloud/bmaas/bmdb/model/migrations/1662136250_initial.down.sql
index 5f336d1..5c0ebe9 100644
--- a/cloud/bmaas/bmdb/model/migrations/1662136250_initial.down.sql
+++ b/cloud/bmaas/bmdb/model/migrations/1662136250_initial.down.sql
@@ -1,3 +1,4 @@
 DROP TABLE work;
 DROP TABLE sessions;
 DROP TABLE machines;
+DROP TYPE process;
\ No newline at end of file
diff --git a/cloud/bmaas/bmdb/model/migrations/1667232160_agent_tags.down.sql b/cloud/bmaas/bmdb/model/migrations/1667232160_agent_tags.down.sql
index 90bb586..8630143 100644
--- a/cloud/bmaas/bmdb/model/migrations/1667232160_agent_tags.down.sql
+++ b/cloud/bmaas/bmdb/model/migrations/1667232160_agent_tags.down.sql
@@ -1,3 +1,5 @@
-DROP TABLE machine_provided;
-DROP TABLE machine_agent_started;
 DROP TABLE machine_agent_heartbeat;
+DROP TABLE machine_agent_started;
+DROP TABLE machine_provided;
+DROP TABLE machine_hardware_report;
+DROP type provider;
diff --git a/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.down.sql b/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.down.sql
index e69de29..0345be6 100644
--- a/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.down.sql
+++ b/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.down.sql
@@ -0,0 +1,2 @@
+DROP TABLE machine_os_installation_report;
+DROP TABLE machine_os_installation_request;
\ No newline at end of file
diff --git a/cloud/lib/component/crdb.go b/cloud/lib/component/crdb.go
index bff98f0..0de8bcf 100644
--- a/cloud/lib/component/crdb.go
+++ b/cloud/lib/component/crdb.go
@@ -3,6 +3,7 @@
 import (
 	"database/sql"
 	"flag"
+	"fmt"
 	"net/url"
 	"os"
 	"sync"
@@ -142,3 +143,38 @@
 	}
 	return m.Up()
 }
+
+// MigrateDownDangerDanger removes all data from the database by performing a
+// full migration down.
+//
+// Let me reiterate: this function, by design, DESTROYS YOUR DATA.
+//
+// Obviously, this is a dangerous method. Thus, to prevent accidental nuking of
+// production data, we currently only allow this to be performed on InMemory
+// databases.
+func (d *CockroachConfig) MigrateDownDangerDanger() error {
+	if !d.InMemory {
+		return fmt.Errorf("refusing to migrate down a non-in-memory database")
+	}
+	// Sneaky extra check to make sure the caller didn't just set InMemory after
+	// connecting to an external database. We really need to be safe here.
+	if d.inMemoryInstance == nil {
+		return fmt.Errorf("no really, this cannot be run on non-in-memory databases")
+	}
+	dsn := d.buildDSN("cockroachdb")
+	klog.Infof("Running migrations on %s...", dsn)
+	m, err := migrate.NewWithSourceInstance("iofs", d.Migrations, dsn)
+	if err != nil {
+		return err
+	}
+	// Final sneaky check, make sure the remote schema version is our maximum locally
+	// supported version.
+	v, _, err := m.Version()
+	if err != nil {
+		return fmt.Errorf("could not retrieve remote version: %w", err)
+	}
+	if v2, err := d.Migrations.Next(v); !os.IsNotExist(err) {
+		return fmt.Errorf("remote running version %d, but we know %d which is newer", v, v2)
+	}
+	return m.Down()
+}