Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions opensabre-base-dependencies/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<swagger-annotations.version>2.2.47</swagger-annotations.version>
<!--数据库相关-->
<mysql-connector-j.version>8.2.0</mysql-connector-j.version>
<flyway.version>11.18.0</flyway.version>
<mybatis-plus-boot-starter.version>3.5.17</mybatis-plus-boot-starter.version>
<mybatis-spring.version>4.0.0</mybatis-spring.version>
<!--中间件-->
Expand Down Expand Up @@ -128,6 +129,11 @@
<version>${swagger-annotations.version}</version>
</dependency>
<!--opensabre 组件-->
<dependency>
<groupId>io.github.opensabre</groupId>
<artifactId>opensabre-starter-migration</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>io.github.opensabre</groupId>
<artifactId>opensabre-web</artifactId>
Expand Down Expand Up @@ -226,6 +232,16 @@
<version>${mybatis-spring.version}</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
Expand Down
51 changes: 51 additions & 0 deletions opensabre-starter-migration/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.github.opensabre</groupId>
<artifactId>opensabre-framework</artifactId>
<version>${revision}</version>
</parent>

<artifactId>opensabre-starter-migration</artifactId>
<packaging>jar</packaging>

<name>opensabre-starter-migration</name>
<description>Explicit, process-isolated Flyway migration runner for OpenSabre applications</description>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.opensabre</groupId>
<artifactId>opensabre-base-dependencies</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package io.github.opensabre.migration;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.MigrationInfo;
import org.flywaydb.core.api.output.MigrateResult;

import java.net.URI;
import java.net.URISyntaxException;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Map;

/**
* Runs one application's Flyway migrations as a dedicated process.
*
* <p>The runner intentionally does not start Spring Boot. Deployments invoke it before the
* corresponding application, using the migration scripts and dependencies already contained in
* that application's immutable image.</p>
*/
public final class FlywayMigrationRunner {

private static final String DEFAULT_LOCATIONS = "classpath:db/migration/mysql";

private FlywayMigrationRunner() {
}

/**
* Validates the configured migration history and migrates the database to the requested target.
*
* @param args unused; configuration is supplied through {@code FLYWAY_*} environment variables
*/
public static void main(String[] args) {
Map<String, String> environment = System.getenv();
String url = required(environment, "FLYWAY_URL");
String user = required(environment, "FLYWAY_USER");
String password = required(environment, "FLYWAY_PASSWORD");
String database = required(environment, "FLYWAY_DATABASE");
String target = required(environment, "FLYWAY_TARGET");
String locations = environment.getOrDefault("FLYWAY_LOCATIONS", DEFAULT_LOCATIONS);
validateTarget(url, user, password, database);

var configuration = Flyway.configure()
.dataSource(url, user, password)
.locations(split(locations))
.schemas(database)
.defaultSchema(database)
.createSchemas(false)
.validateMigrationNaming(true)
.validateOnMigrate(true)
.baselineOnMigrate(false)
.cleanDisabled(true)
.target(target);

Flyway flyway = configuration.load();
flyway.validate();
MigrateResult result = flyway.migrate();
MigrationInfo current = flyway.info().current();
String currentVersion = current == null || current.getVersion() == null
? "none"
: current.getVersion().getVersion();
System.out.printf("Flyway migration completed: database=%s, migrations=%d, current=%s%n",
result.database, result.migrationsExecuted, currentVersion);
}

private static String required(Map<String, String> environment, String name) {
String value = environment.get(name);
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing required environment variable: " + name);
}
return value;
}

/**
* Ensures the JDBC URL and the connected MySQL session both point at the explicitly approved
* database. SQL migrations are separately linted to forbid database-switching statements.
*/
private static void validateTarget(String url, String user, String password, String expectedDatabase) {
String urlDatabase = databaseFromUrl(url);
if (!expectedDatabase.equals(urlDatabase)) {
throw new IllegalArgumentException("FLYWAY_DATABASE does not match JDBC URL database: expected="
+ expectedDatabase + ", actual=" + urlDatabase);
}

try (var connection = DriverManager.getConnection(url, user, password)) {
String connectedDatabase = connection.getCatalog();
if (!expectedDatabase.equals(connectedDatabase)) {
throw new IllegalStateException("Connected database does not match FLYWAY_DATABASE: expected="
+ expectedDatabase + ", actual=" + connectedDatabase);
}
} catch (SQLException exception) {
throw new IllegalStateException("Unable to verify Flyway database target", exception);
}
}

private static String databaseFromUrl(String url) {
if (!url.startsWith("jdbc:mysql:")) {
throw new IllegalArgumentException("Only jdbc:mysql URLs are supported: " + url);
}
try {
String path = new URI(url.substring("jdbc:".length())).getPath();
if (path == null || path.length() <= 1 || path.indexOf('/', 1) >= 0) {
throw new IllegalArgumentException("JDBC URL must select exactly one database: " + url);
}
return path.substring(1);
} catch (URISyntaxException exception) {
throw new IllegalArgumentException("Invalid JDBC URL: " + url, exception);
}
}

private static String[] split(String locations) {
return locations.lines()
.flatMap(line -> java.util.Arrays.stream(line.split(",")))
.map(String::trim)
.filter(location -> !location.isEmpty())
.toArray(String[]::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.opensabre.migration;

import org.junit.jupiter.api.Test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* Verifies strict parsing of deployment-supplied migration configuration.
*/
class FlywayMigrationRunnerTest {

@Test
void rejectsMissingRequiredConfiguration() throws Exception {
Method required = FlywayMigrationRunner.class.getDeclaredMethod("required", Map.class, String.class);
required.setAccessible(true);

InvocationTargetException exception = assertThrows(InvocationTargetException.class,
() -> required.invoke(null, Map.of(), "FLYWAY_URL"));

assertEquals("Missing required environment variable: FLYWAY_URL", exception.getCause().getMessage());
}

@Test
void extractsTheSingleDatabaseSelectedByMysqlUrl() throws Exception {
Method databaseFromUrl = FlywayMigrationRunner.class.getDeclaredMethod("databaseFromUrl", String.class);
databaseFromUrl.setAccessible(true);

assertEquals("flyway_test_auth",
databaseFromUrl.invoke(null, "jdbc:mysql://mysql:3306/flyway_test_auth?useSSL=false"));
}

@Test
void rejectsMysqlUrlWithoutDatabase() throws Exception {
Method databaseFromUrl = FlywayMigrationRunner.class.getDeclaredMethod("databaseFromUrl", String.class);
databaseFromUrl.setAccessible(true);

InvocationTargetException exception = assertThrows(InvocationTargetException.class,
() -> databaseFromUrl.invoke(null, "jdbc:mysql://mysql:3306/?useSSL=false"));

assertEquals("JDBC URL must select exactly one database: jdbc:mysql://mysql:3306/?useSSL=false",
exception.getCause().getMessage());
}
}
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<module>opensabre-starter-register</module>
<module>opensabre-starter-rpc</module>
<module>opensabre-starter-persistence</module>
<module>opensabre-starter-migration</module>
<module>opensabre-starter-eda</module>
<module>opensabre-starter-governance</module>
<module>opensabre-starter-security</module>
Expand Down Expand Up @@ -68,7 +69,7 @@
<central-publishing-maven-plugin.version>0.9.0</central-publishing-maven-plugin.version>
<lombok.version>1.18.42</lombok.version>

<revision>1.1.2</revision>
<revision>1.1.3-SNAPSHOT</revision>
</properties>

<!-- 发布配置 -->
Expand Down
Loading