Skip to content

Commit

Permalink
[Backport 2.9.1][PLAT-2157] Flyway plugin patch for ignoreMissingMigr…
Browse files Browse the repository at this point in the history
…ation and default java package issue

Summary:
Also `ensureJavaFriendlyLocation` to workaround default being reserved keyword in java
   We will allow for adding java classes to db/migration/default_/common (Note the underscore) when dbName is default
For `ignoreMissingMigrations` issue See [[ https://docs.google.com/document/d/1U_jEvOfTS3ofD-3NDL21soQxYxTu0pmdd-DEKZu_ynA/edit# | DB Migration Versioning Background ]]
Our design options are limited to just Option#2 Use repeatable migrations for backport.

This is very limiting and something we would like to get out of.
We are specifically blocked by
playframework/flyway-play#96 which has not seen any traction.

At the same time we do not want to hand-roll our own flyway-play plugin and config mechanism
mainly due to additional testing overhead.
Also one day we will migrate to play 2.8.X and then we can upgrade to latest plugin with
all the fixes we want.

The code is identical to:
  org.flywaydb.play.PlayInitializer
Only addition of
  ybPatchAdditionalConfigurations and ensureJavaFriendlyLocation methods that get called during configuring flyway.
Rest of the classes are being used from plugin as-is
Ideally could have used inheritance and not repeated even this class's code. Unfortunately all the interesting methods and properties needed
to do what we want; were private.

Original Commit: D13710 / 81d2902

Test Plan:
Unfortunatly resources in test path are not picked up by fakedbapp. So had to resort to manual testing.

- Deleted  V98 migration.
- Got error
- Disable flyway plugin and set `ignoreMissingMigrations=true` in `application.conf`
- No error during startup.

Reviewers: spotachev, sanketh, amalyshev

Subscribers: yugaware

Differential Revision: https://phabricator.dev.yugabyte.com/D13976
  • Loading branch information
sb-yb committed Nov 17, 2021
1 parent 4d60fd7 commit 6ce5128
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 3 deletions.
6 changes: 4 additions & 2 deletions managed/src/main/java/Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import com.yugabyte.yw.cloud.AWSInitializer;
import com.yugabyte.yw.cloud.aws.AWSCloudModule;
import com.yugabyte.yw.commissioner.CallHome;
import com.yugabyte.yw.common.metrics.PlatformMetricsProcessor;
import com.yugabyte.yw.commissioner.HealthChecker;
import com.yugabyte.yw.common.alerts.QueryAlerts;
import com.yugabyte.yw.commissioner.SetUniverseKey;
import com.yugabyte.yw.commissioner.TaskGarbageCollector;
import com.yugabyte.yw.common.AccessManager;
Expand All @@ -29,14 +27,17 @@
import com.yugabyte.yw.common.YsqlQueryExecutor;
import com.yugabyte.yw.common.alerts.AlertConfigurationWriter;
import com.yugabyte.yw.common.alerts.AlertsGarbageCollector;
import com.yugabyte.yw.common.alerts.QueryAlerts;
import com.yugabyte.yw.common.config.RuntimeConfigFactory;
import com.yugabyte.yw.common.config.impl.SettableRuntimeConfigFactory;
import com.yugabyte.yw.common.ha.PlatformReplicationHelper;
import com.yugabyte.yw.common.ha.PlatformReplicationManager;
import com.yugabyte.yw.common.kms.EncryptionAtRestManager;
import com.yugabyte.yw.common.kms.util.EncryptionAtRestUniverseKeyCache;
import com.yugabyte.yw.common.metrics.PlatformMetricsProcessor;
import com.yugabyte.yw.common.services.LocalYBClientService;
import com.yugabyte.yw.common.services.YBClientService;
import com.yugabyte.yw.common.ybflyway.YBFlywayInit;
import com.yugabyte.yw.controllers.PlatformHttpActionAdapter;
import com.yugabyte.yw.metrics.MetricQueryHelper;
import com.yugabyte.yw.queries.QueryHelper;
Expand Down Expand Up @@ -69,6 +70,7 @@ public Module(Environment environment, Configuration config) {

@Override
public void configure() {
bind(YBFlywayInit.class).asEagerSingleton();
bind(RuntimeConfigFactory.class).to(SettableRuntimeConfigFactory.class).asEagerSingleton();
// TODO: other clouds
install(new AWSCloudModule());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package com.yugabyte.yw.common.ybflyway

import java.io.FileNotFoundException

import javax.inject._
import org.flywaydb.core.Flyway
import org.flywaydb.core.api.MigrationInfo
import org.flywaydb.core.internal.util.jdbc.DriverDataSource
import org.flywaydb.play._
import play.api._
import play.core._

import scala.collection.JavaConverters._

/**
* This class is mostly similar to org.flywaydb.play.PlayInitializer from the flyway-play plugin
* Having to do this so that we can workaround the bugs in the plugin.
*
* https://github.com/flyway/flyway-play/issues/96
* Note: All the workaround logic is in methods are implemented as companion methods in YBFlywayInit
*/
@Singleton
class YBFlywayInit @Inject()(
appConfiguration: Configuration,
environment: Environment,
webCommands: WebCommands
) {
private val flywayConfigurations = {
val configReader = new ConfigReader(appConfiguration, environment)
configReader.getFlywayConfigurations
}

private val allDatabaseNames = flywayConfigurations.keys

private val flywayPrefixToMigrationScript = "db/migration"

private def migrationFileDirectoryExists(path: String): Boolean = {
environment.resource(path) match {
case Some(_) =>
Logger.debug(s"Directory for migration files found. $path")
true
case None =>
Logger.warn(s"Directory for migration files not found. $path")
false
}
}

private lazy val flyways: Map[String, Flyway] = {
for {
(dbName, flywayConfiguration) <- flywayConfigurations
migrationFilesLocation = s"$flywayPrefixToMigrationScript/$dbName"
if migrationFileDirectoryExists(migrationFilesLocation)
} yield {
val flyway = new Flyway
val database = flywayConfiguration.database
val dataSource = new DriverDataSource(
getClass.getClassLoader,
database.driver,
database.url,
database.user,
database.password,
null
)
flyway.setDataSource(dataSource)
if (flywayConfiguration.locations.nonEmpty) {
YBFlywayInit.ensureJavaFriendlyLocation(dbName,
flywayConfiguration,
migrationFilesLocation,
flyway)
} else {
flyway.setLocations(migrationFilesLocation)
}
flywayConfiguration.encoding.foreach(flyway.setEncoding)
flyway.setSchemas(flywayConfiguration.schemas: _*)
flywayConfiguration.table.foreach(flyway.setTable)
flywayConfiguration.placeholderReplacement.foreach(flyway.setPlaceholderReplacement)
flyway.setPlaceholders(flywayConfiguration.placeholders.asJava)
flywayConfiguration.placeholderPrefix.foreach(flyway.setPlaceholderPrefix)
flywayConfiguration.placeholderSuffix.foreach(flyway.setPlaceholderSuffix)
flywayConfiguration.sqlMigrationPrefix.foreach(flyway.setSqlMigrationPrefix)
flywayConfiguration
.repeatableSqlMigrationPrefix
.foreach(flyway.setRepeatableSqlMigrationPrefix)
flywayConfiguration.sqlMigrationSeparator.foreach(flyway.setSqlMigrationSeparator)
flywayConfiguration.sqlMigrationSuffix.foreach(flyway.setSqlMigrationSuffix)
flywayConfiguration.ignoreFutureMigrations.foreach(flyway.setIgnoreFutureMigrations)
flywayConfiguration.validateOnMigrate.foreach(flyway.setValidateOnMigrate)
flywayConfiguration.cleanOnValidationError.foreach(flyway.setCleanOnValidationError)
flywayConfiguration.cleanDisabled.foreach(flyway.setCleanDisabled)
flywayConfiguration.initOnMigrate.foreach(flyway.setBaselineOnMigrate)
flywayConfiguration.outOfOrder.foreach(flyway.setOutOfOrder)

YBFlywayInit.ybPatchAdditionalConfigurations(dbName, flyway, appConfiguration)

dbName -> flyway
}
}

private def migrationDescriptionToShow(dbName: String, migration: MigrationInfo): String = {
val locations = flywayConfigurations(dbName).locations
(if (locations.nonEmpty) {
locations.map(location => environment
.resourceAsStream(
s"$flywayPrefixToMigrationScript/$dbName/$location/${migration.getScript}"))
.find(resource => resource.nonEmpty).flatten
} else {
environment.resourceAsStream(s"$flywayPrefixToMigrationScript/$dbName/${migration.getScript}")
}).map { in =>
s"""|--- ${migration.getScript} ---
|${FileUtils.readInputStreamToString(in)}""".stripMargin
}.orElse {
import scala.util.control.Exception._
val code = for {
script <- FileUtils.findJdbcMigrationFile(environment.rootPath, migration.getScript)
} yield FileUtils.readFileToString(script)
allCatch opt {
environment.classLoader.loadClass(migration.getScript)
} map { _ =>
s"""|--- ${migration.getScript} ---
|$code""".stripMargin
}
}.getOrElse(throw new FileNotFoundException(
s"Migration file not found. ${migration.getScript}"))
}

private def checkState(dbName: String): Unit = {
flyways.get(dbName).foreach { flyway =>
val pendingMigrations = flyway.info().pending
if (pendingMigrations.nonEmpty) {
throw InvalidDatabaseRevision(
dbName,
pendingMigrations
.map(migration => migrationDescriptionToShow(dbName, migration))
.mkString("\n")
)
}

if (flywayConfigurations(dbName).validateOnStart) {
flyway.validate()
}
}
}

def onStart(): Unit = {
val flywayWebCommand = new FlywayWebCommand(
appConfiguration, environment, flywayPrefixToMigrationScript, flyways)
webCommands.addHandler(flywayWebCommand)

for (dbName <- allDatabaseNames) {
if (environment.mode == Mode.Test || flywayConfigurations(dbName).auto) {
migrateAutomatically(dbName)
} else {
checkState(dbName)
}
}
}

private def migrateAutomatically(dbName: String): Unit = {
flyways.get(dbName).foreach { flyway =>
flyway.migrate()
}
}

val enabled: Boolean =
!appConfiguration.getOptional[String]("flywayplugin").contains("disabled")

if (enabled) {
onStart()
}

}

object YBFlywayInit {

// Additional configuration not handled by the older plugin version
private def ybPatchAdditionalConfigurations(dbName: String,
flyway: Flyway,
configuration: Configuration): Unit = {
val subConfig = configuration.getOptional[Configuration](s"db.$dbName.migration")
.getOrElse(Configuration.empty)
subConfig.getOptional[Boolean]("ignoreMissingMigrations")
.foreach(flyway.setIgnoreMissingMigrations)
}

private def ensureJavaFriendlyLocation(dbName: String,
flywayConfiguration: FlywayConfiguration,
migrationFilesLocation: String,
flyway: Flyway): Unit = {
val locations = flywayConfiguration
.locations
.map(location => s"$migrationFilesLocation/$location")
if (dbName == "default") {
val javaFriendlyLocations = flywayConfiguration
.locations
.map(location => s"${migrationFilesLocation}_/$location")
flyway.setLocations(javaFriendlyLocations ++ locations: _*)
} else {
flyway.setLocations(locations: _*)
}
}
}
4 changes: 3 additions & 1 deletion managed/src/main/resources/application.test.conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
include classpath("application.common.conf")

play.evolutions.enabled=false
play.modules.enabled += "org.flywaydb.play.PlayModule"

# We use our own flyway initializer see: com.yugabyte.yw.common.ybflyway.PlayInitializer
#play.modules.enabled += "org.flywaydb.play.PlayModule"

db {
default.driver="org.h2.Driver"
Expand Down

0 comments on commit 6ce5128

Please sign in to comment.