Skip to content

Commit

Permalink
ci: allow to run the application only to validate Liquibase migrations
Browse files Browse the repository at this point in the history
Now the application can be run as
    java -jar target/mystamps.war liquibase validate
or
    ./mvnw spring-boot:run -Dspring-boot.run.arguments='liquibase,validate'
to validate that all migrations and their checksums are correct. This should prevent the case when
the application is failing to start after deploy as now we have a possibility to check that
migrations are valid prior the application is run.

Part of #383
  • Loading branch information
php-coder committed Jan 30, 2022
1 parent 547cfd1 commit 3c9c6a7
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 2 deletions.
1 change: 0 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
Expand Down
141 changes: 141 additions & 0 deletions src/main/java/ru/mystamps/web/support/liquibase/LiquibaseSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright (C) 2009-2022 Slava Semushin <[email protected]>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package ru.mystamps.web.support.liquibase;

import liquibase.Liquibase;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.integration.spring.SpringLiquibase;
import liquibase.integration.spring.SpringResourceAccessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;

/**
* Provides ability to run Spring Boot application to only validate Liquibase migrations.
*/
public final class LiquibaseSupport {

private LiquibaseSupport() {
}

public static SpringApplication createSpringApplication() {
// Don't run Liquibase by default, we only need to initialize all required beans
// Note that we can't set "spring.liquibase.enabled: false" because it disables
// autoconfiguration of Liquibase beans completely.
// See https://docs.liquibase.com/commands/config-ref/should-run-parameter.html
System.setProperty("liquibase.shouldRun", "false");

// Explicitly disable JMX. It might be enabled when we run via maven
System.setProperty("spring.jmx.enabled", "false");

// Override value (WARN) from application*.properties
System.setProperty("logging.level.liquibase", "INFO");

// LATER: Ideally, we don't need to use a connection pool (HikariCP) in this case.
// Consider configuring spring.datasource.type property.
SpringApplication app = new SpringApplication(LiquibaseOnlyStartup.class);

// Act as a console application instead of as a web application.
// See https://www.baeldung.com/spring-boot-no-web-server
app.setDefaultProperties(
Collections.singletonMap("spring.main.web-application-type", "none")
);

return app;
}

public static void validate(ApplicationContext context) throws LiquibaseException {
SpringLiquibase springLiquibase = context.getBean(SpringLiquibase.class);
performLiquibaseValidate(springLiquibase);
}

@Import({
DataSourceAutoConfiguration.class,
LiquibaseAutoConfiguration.class
})
public static class LiquibaseOnlyStartup {
}

// CheckStyle: ignore LineLength for next 2 lines
// Partially copy&pasted from:
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/integration/spring/SpringLiquibase.java#L263-L276
// Reason: the original code executes "update" while we need to perform validation
private static void performLiquibaseValidate(SpringLiquibase springLiquibase)
throws LiquibaseException {
// CheckStyle: ignore LineLength for next 1 line
try (Liquibase liquibase = createLiquibase(springLiquibase.getDataSource().getConnection(), springLiquibase)) {
validate(liquibase, springLiquibase);
} catch (SQLException ex) {
throw new DatabaseException(ex);
}
}

// CheckStyle: ignore LineLength for next 2 lines
// Partially copy&pasted from:
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/Liquibase.java#L2279-L2283
// Reason: the original method doesn't respect spring.liquibase.contexts
// NOTE: spring.liquibase.labels aren't supported as we don't use them
private static void validate(Liquibase liquibase, SpringLiquibase springLiquibase)
throws LiquibaseException {
DatabaseChangeLog changeLog = liquibase.getDatabaseChangeLog();
changeLog.validate(liquibase.getDatabase(), springLiquibase.getContexts());
}

// CheckStyle: ignore LineLength for next 2 lines
// Partially copy&pasted from:
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/integration/spring/SpringLiquibase.java#L320-L334
// Reason: the original method is protected
// NOTE: spring.liquibase.parameters.* aren't supported as we don't have access to it
// (SpringLiquibase doesn't have a getter)
private static Liquibase createLiquibase(Connection conn, SpringLiquibase springLiquibase)
throws DatabaseException {
return new Liquibase(
springLiquibase.getChangeLog(),
new SpringResourceAccessor(springLiquibase.getResourceLoader()),
createDatabase(conn)
);
}

// CheckStyle: ignore LineLength for next 2 lines
// Partially copy&pasted from:
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/integration/spring/SpringLiquibase.java#L344-L380
// Reason: the original method is protected
// NOTE: the following parameter aren't supported (as we don't use them):
// - spring.liquibase.default-schema
// - spring.liquibase.liquibase-schema
// - spring.liquibase.liquibase-tablespace
// - spring.liquibase.database-change-log-table
// - spring.liquibase.database-change-log-lock-table
private static Database createDatabase(Connection conn) throws DatabaseException {
return DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(conn));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Integration with <a href="https://liquibase.org" target="_blank">Liquibase</a>.
*/
package ru.mystamps.web.support.liquibase;
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package ru.mystamps.web.support.spring.boot;

import liquibase.exception.LiquibaseException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
Expand All @@ -25,13 +26,33 @@
import org.togglz.core.manager.FeatureManager;
import ru.mystamps.web.config.ApplicationContext;
import ru.mystamps.web.config.DispatcherServletContext;
import ru.mystamps.web.support.liquibase.LiquibaseSupport;

// PMD: "All methods are static" here because it's a program entry point.
// CheckStyle: I cannot declare the constructor as private because app won't start.
@SuppressWarnings({ "PMD.UseUtilityClass", "checkstyle:hideutilityclassconstructor" })
public class ApplicationBootstrap {

public static void main(String... args) {
public static void main(String... args) throws LiquibaseException {
// When the application is started as
//
// java -jar target/mystamps.war liquibase validate
// or
// ./mvnw spring-boot:run -Dspring-boot.run.arguments='liquibase,validate'
//
// we don't run a full application but loads only Liquibase-related classes
boolean executeOnlyLiquibase = args.length == 2
&& "liquibase".equals(args[0])
&& "validate".equals(args[1]);
if (executeOnlyLiquibase) {
ConfigurableApplicationContext context = LiquibaseSupport
.createSpringApplication()
.run(args);

LiquibaseSupport.validate(context);
return;
}

ConfigurableApplicationContext context =
SpringApplication.run(DefaultStartup.class, args);

Expand Down

0 comments on commit 3c9c6a7

Please sign in to comment.