Skip to content

Commit 3c9c6a7

Browse files
committed
ci: allow to run the application only to validate Liquibase migrations
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
1 parent 547cfd1 commit 3c9c6a7

File tree

4 files changed

+167
-2
lines changed

4 files changed

+167
-2
lines changed

Diff for: pom.xml

-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@
220220
<groupId>org.liquibase</groupId>
221221
<artifactId>liquibase-core</artifactId>
222222
<version>${liquibase.version}</version>
223-
<scope>runtime</scope>
224223
<exclusions>
225224
<exclusion>
226225
<groupId>org.yaml</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (C) 2009-2022 Slava Semushin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
19+
package ru.mystamps.web.support.liquibase;
20+
21+
import liquibase.Liquibase;
22+
import liquibase.changelog.DatabaseChangeLog;
23+
import liquibase.database.Database;
24+
import liquibase.database.DatabaseFactory;
25+
import liquibase.database.jvm.JdbcConnection;
26+
import liquibase.exception.DatabaseException;
27+
import liquibase.exception.LiquibaseException;
28+
import liquibase.integration.spring.SpringLiquibase;
29+
import liquibase.integration.spring.SpringResourceAccessor;
30+
import org.springframework.boot.SpringApplication;
31+
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
32+
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
33+
import org.springframework.context.ApplicationContext;
34+
import org.springframework.context.annotation.Import;
35+
36+
import java.sql.Connection;
37+
import java.sql.SQLException;
38+
import java.util.Collections;
39+
40+
/**
41+
* Provides ability to run Spring Boot application to only validate Liquibase migrations.
42+
*/
43+
public final class LiquibaseSupport {
44+
45+
private LiquibaseSupport() {
46+
}
47+
48+
public static SpringApplication createSpringApplication() {
49+
// Don't run Liquibase by default, we only need to initialize all required beans
50+
// Note that we can't set "spring.liquibase.enabled: false" because it disables
51+
// autoconfiguration of Liquibase beans completely.
52+
// See https://docs.liquibase.com/commands/config-ref/should-run-parameter.html
53+
System.setProperty("liquibase.shouldRun", "false");
54+
55+
// Explicitly disable JMX. It might be enabled when we run via maven
56+
System.setProperty("spring.jmx.enabled", "false");
57+
58+
// Override value (WARN) from application*.properties
59+
System.setProperty("logging.level.liquibase", "INFO");
60+
61+
// LATER: Ideally, we don't need to use a connection pool (HikariCP) in this case.
62+
// Consider configuring spring.datasource.type property.
63+
SpringApplication app = new SpringApplication(LiquibaseOnlyStartup.class);
64+
65+
// Act as a console application instead of as a web application.
66+
// See https://www.baeldung.com/spring-boot-no-web-server
67+
app.setDefaultProperties(
68+
Collections.singletonMap("spring.main.web-application-type", "none")
69+
);
70+
71+
return app;
72+
}
73+
74+
public static void validate(ApplicationContext context) throws LiquibaseException {
75+
SpringLiquibase springLiquibase = context.getBean(SpringLiquibase.class);
76+
performLiquibaseValidate(springLiquibase);
77+
}
78+
79+
@Import({
80+
DataSourceAutoConfiguration.class,
81+
LiquibaseAutoConfiguration.class
82+
})
83+
public static class LiquibaseOnlyStartup {
84+
}
85+
86+
// CheckStyle: ignore LineLength for next 2 lines
87+
// Partially copy&pasted from:
88+
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/integration/spring/SpringLiquibase.java#L263-L276
89+
// Reason: the original code executes "update" while we need to perform validation
90+
private static void performLiquibaseValidate(SpringLiquibase springLiquibase)
91+
throws LiquibaseException {
92+
// CheckStyle: ignore LineLength for next 1 line
93+
try (Liquibase liquibase = createLiquibase(springLiquibase.getDataSource().getConnection(), springLiquibase)) {
94+
validate(liquibase, springLiquibase);
95+
} catch (SQLException ex) {
96+
throw new DatabaseException(ex);
97+
}
98+
}
99+
100+
// CheckStyle: ignore LineLength for next 2 lines
101+
// Partially copy&pasted from:
102+
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/Liquibase.java#L2279-L2283
103+
// Reason: the original method doesn't respect spring.liquibase.contexts
104+
// NOTE: spring.liquibase.labels aren't supported as we don't use them
105+
private static void validate(Liquibase liquibase, SpringLiquibase springLiquibase)
106+
throws LiquibaseException {
107+
DatabaseChangeLog changeLog = liquibase.getDatabaseChangeLog();
108+
changeLog.validate(liquibase.getDatabase(), springLiquibase.getContexts());
109+
}
110+
111+
// CheckStyle: ignore LineLength for next 2 lines
112+
// Partially copy&pasted from:
113+
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/integration/spring/SpringLiquibase.java#L320-L334
114+
// Reason: the original method is protected
115+
// NOTE: spring.liquibase.parameters.* aren't supported as we don't have access to it
116+
// (SpringLiquibase doesn't have a getter)
117+
private static Liquibase createLiquibase(Connection conn, SpringLiquibase springLiquibase)
118+
throws DatabaseException {
119+
return new Liquibase(
120+
springLiquibase.getChangeLog(),
121+
new SpringResourceAccessor(springLiquibase.getResourceLoader()),
122+
createDatabase(conn)
123+
);
124+
}
125+
126+
// CheckStyle: ignore LineLength for next 2 lines
127+
// Partially copy&pasted from:
128+
// https://github.com/liquibase/liquibase/blob/v4.7.1/liquibase-core/src/main/java/liquibase/integration/spring/SpringLiquibase.java#L344-L380
129+
// Reason: the original method is protected
130+
// NOTE: the following parameter aren't supported (as we don't use them):
131+
// - spring.liquibase.default-schema
132+
// - spring.liquibase.liquibase-schema
133+
// - spring.liquibase.liquibase-tablespace
134+
// - spring.liquibase.database-change-log-table
135+
// - spring.liquibase.database-change-log-lock-table
136+
private static Database createDatabase(Connection conn) throws DatabaseException {
137+
return DatabaseFactory.getInstance()
138+
.findCorrectDatabaseImplementation(new JdbcConnection(conn));
139+
}
140+
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Integration with <a href="https://liquibase.org" target="_blank">Liquibase</a>.
3+
*/
4+
package ru.mystamps.web.support.liquibase;

Diff for: src/main/java/ru/mystamps/web/support/spring/boot/ApplicationBootstrap.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package ru.mystamps.web.support.spring.boot;
1919

20+
import liquibase.exception.LiquibaseException;
2021
import org.springframework.boot.SpringApplication;
2122
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2223
import org.springframework.context.ConfigurableApplicationContext;
@@ -25,13 +26,33 @@
2526
import org.togglz.core.manager.FeatureManager;
2627
import ru.mystamps.web.config.ApplicationContext;
2728
import ru.mystamps.web.config.DispatcherServletContext;
29+
import ru.mystamps.web.support.liquibase.LiquibaseSupport;
2830

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

34-
public static void main(String... args) {
36+
public static void main(String... args) throws LiquibaseException {
37+
// When the application is started as
38+
//
39+
// java -jar target/mystamps.war liquibase validate
40+
// or
41+
// ./mvnw spring-boot:run -Dspring-boot.run.arguments='liquibase,validate'
42+
//
43+
// we don't run a full application but loads only Liquibase-related classes
44+
boolean executeOnlyLiquibase = args.length == 2
45+
&& "liquibase".equals(args[0])
46+
&& "validate".equals(args[1]);
47+
if (executeOnlyLiquibase) {
48+
ConfigurableApplicationContext context = LiquibaseSupport
49+
.createSpringApplication()
50+
.run(args);
51+
52+
LiquibaseSupport.validate(context);
53+
return;
54+
}
55+
3556
ConfigurableApplicationContext context =
3657
SpringApplication.run(DefaultStartup.class, args);
3758

0 commit comments

Comments
 (0)