com.liquibase.ext.tools.MongoshRunner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of liquibase-commercial-mongodb Show documentation
Show all versions of liquibase-commercial-mongodb Show documentation
Liquibase Commercial Extension for MongoDB
The newest version!
package com.liquibase.ext.tools;
import com.datical.liquibase.ext.util.NativeRunnerUtil;
import com.liquibase.ext.config.MongoshConfiguration;
import liquibase.Scope;
import liquibase.change.core.ExecuteShellCommandChange;
import liquibase.changelog.ChangeSet;
import liquibase.database.Database;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.ext.mongodb.database.MongoConnection;
import liquibase.ext.mongodb.database.MongoLiquibaseDatabase;
import liquibase.logging.Logger;
import liquibase.resource.Resource;
import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.LiquibaseService;
import liquibase.sql.Sql;
import liquibase.util.StringUtil;
import lombok.Getter;
import lombok.Setter;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.concurrent.TimeoutException;
import static com.liquibase.ext.config.MongoshConfiguration.ConfigurationKeys.*;
import static com.datical.liquibase.ext.util.NativeRunnerUtil.getBooleanFromProperties;
import static com.datical.liquibase.ext.util.NativeRunnerUtil.validateTimeout;
@LiquibaseService(skip = true)
public class MongoshRunner extends ExecuteShellCommandChange {
private ChangeSet changeSet;
private Sql[] sqlStrings;
private File outFile = null;
private Boolean keepTempFile = false;
private List args = new ArrayList<>();
private String tempName;
private String tempPath;
private String logFile;
private Integer timeout;
private File mongoshExec;
private static final String EXECUTABLE_NAME = "mongosh";
private static final String MONGOSH_CONF = "liquibase.mongosh.conf";
private static final ResourceBundle mongoshBundle = ResourceBundle.getBundle("liquibase/i18n/liquibase-mongosh");
private static final String MSG_UNABLE_TO_RUN_MONGOSH = mongoshBundle.getString("unable.to.run.mongosh");
public MongoshRunner() {
}
/**
* Constructor which is normally used
*
* @param changeSet The current change set
* @param sqlStrings SQL that will be executed
*/
public MongoshRunner(ChangeSet changeSet, Sql[] sqlStrings) {
this.changeSet = changeSet;
this.sqlStrings = sqlStrings;
setTimeout("1800");
}
/**
* This method creates the command line arguments that are specific to mongosh execution.
*
* @param database The Database we are working on
* @return List The command args in a List
*/
@Override
protected List createFinalCommandArray(Database database) {
loadMongoshProperties();
List commandArray = super.createFinalCommandArray(database);
try {
writeSqlStrings();
} catch (Exception ioe) {
throw new UnexpectedLiquibaseException(ioe);
}
if (!args.isEmpty()) {
commandArray.addAll(Collections.unmodifiableList(args));
}
if (sqlStrings != null) { //if we have a script
MongoLiquibaseDatabase mongoDatabase = (MongoLiquibaseDatabase) database;
MongoConnection connection = (MongoConnection) mongoDatabase.getConnection();
commandArray.add(connection.getConnectionString().getConnectionString());
if (outFile != null) { //if we have a script
commandArray.add("--file");
commandArray.add(outFile.getAbsolutePath());
} else {
//TODO we may not need it at all
commandArray.add("--eval");
commandArray.add(this.sqlStrings[0].toSql());
}
} else {
//otherwise, we're checking to make sure mongosh exists sending --version flag
commandArray.add("--version");
}
String commandLine = StringUtil.join(commandArray, " ");
//command could contain something like this: `mongodb://uname:pass@localhost:27017/lbcat`
//if that's the case, don't log the exact string
Scope.getCurrentScope().getLog(getClass()).info("mongosh command:\n" + commandLine.replaceAll("://.*:.*@", "://@"));
return commandArray;
}
/**
* Process the result. If we get an exception and there is output from the command output stream then add
* that to the exception message.
*
* @param returnCode - it's an error code from SqlPlus DB client
* @param errorStreamOut - error stream from processing sql
* @param infoStreamOut - stream from processing sql
* @param database - db object we use in process
* @throws UnexpectedLiquibaseException when return code is non-zero
*/
@Override
protected void processResult(int returnCode, String errorStreamOut, String infoStreamOut, Database database) {
if (logFile != null && outFile != null) {
//TODO figure out if we can get underlying error when mongosh return error code
//mongosh --output and --log-file arguments will only send a small subset of relevant logs to the files specified.
//Instead of using those we need to write the output streams to the log file ourselves.
try {
if (!infoStreamOut.isEmpty()) {
Files.write(Paths.get(logFile), infoStreamOut.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
if (!errorStreamOut.isEmpty()) {
Files.write(Paths.get(logFile), errorStreamOut.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
} catch (IOException writeException) {
throw new UnexpectedLiquibaseException(writeException);
}
}
if (returnCode != 0 && !StringUtil.isEmpty(infoStreamOut)) {
String returnString = getCommandString() + " returned a code of " + returnCode + "\n" + infoStreamOut + "Learn more at https://docs.liquibase.com/mongodb";
throw new UnexpectedLiquibaseException(returnString);
}
super.processResult(returnCode, errorStreamOut, infoStreamOut, database);
}
private void writeSqlStrings() throws Exception {
if (sqlStrings == null || sqlStrings.length == 0) {
return;
}
final Logger log = Scope.getCurrentScope().getLog(getClass());
log.info("Creating the mongosh run script");
MongoshFileCreator mongoshFileCreator = new MongoshFileCreator(changeSet, tempName, tempPath, true,
keepTempFile == null ? MongoshConfiguration.TEMP_KEEP.getDefaultValue() : keepTempFile);
try {
outFile = mongoshFileCreator.generateTemporaryFile(".txt");
} catch (IOException e) {
throw new UnexpectedLiquibaseException(e);
}
Path path = Paths.get(outFile.getAbsolutePath());
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
for (Sql sql : sqlStrings) {
String line = sql.toSql();
line = line.replace("\r", "");
writer.write(line);
}
writer.write(";\n");
}
}
/**
* Execute the mongosh command
* The base class handles all the I/O issues from shelling out to run the command
*
* @param database The Database we are working against
* @throws IllegalArgumentException when invalid arguments are found
* @throws LiquibaseException when any other error is encountered
*/
@Override
public void executeCommand(Database database) throws Exception {
try {
this.finalCommandArray = createFinalCommandArray(database);
super.executeCommand(database);
} catch (TimeoutException e) {
try {
Thread.sleep(10000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
processResult(0, null, null, database);
String message = e.getMessage() + System.lineSeparator() + "Error: The mongosh executable failed to return a response with the configured timeout. " +
"Please check liquibase.mongosh.timeout specified in liquibase.mongosh.conf file, the LIQUIBASE_MONGOSH_TIMEOUT environment variable, " +
"or other config locations. Learn more at https://docs.liquibase.com/concepts/advanced/runwith.html and https://docs.liquibase.com/mongodb" + System.lineSeparator();
Scope.getCurrentScope().getUI().sendMessage("WARNING: " + message);
Scope.getCurrentScope().getLog(MongoshRunner.class).warning(message);
throw new LiquibaseException(e);
} catch (IOException e) {
if (e.getMessage().contains("mongosh")) {
throw new LiquibaseException(MSG_UNABLE_TO_RUN_MONGOSH, e);
}
throw new LiquibaseException(e);
} catch (Exception e) {
throw new LiquibaseException(e);
} finally {
if (outFile != null && outFile.exists() && keepTempFile != null && keepTempFile) {
Scope.getCurrentScope().getLog(getClass()).info("Mongosh run script can be located at: " + outFile.getAbsolutePath());
}
}
}
/**
* Base mongosh configuration loader.
*/
private void loadMongoshProperties() {
setExecutable(NativeRunnerUtil.getExecutable(EXECUTABLE_NAME));
Properties properties = getPropertiesFromConf(MONGOSH_CONF);
setupConfProperties(properties);
assignPropertiesFromConfiguration();
handleMongoShExecutable(mongoshExec);
handleTimeout(timeout);
logProperties();
}
//TODO this mostly duplicate NativeExecutorRunner's method that accepts enum, need to rework ConfigFile enum to java class that allow inheritance
public Properties getPropertiesFromConf(String configFile) {
Properties properties = new Properties();
ResourceAccessor resourceAccessor = Scope.getCurrentScope().getResourceAccessor();
InputStream is = null;
try {
Resource resource = resourceAccessor.get(configFile);
if (!resource.exists()) {
Scope.getCurrentScope().getLog(getClass()).info(String.format("No configuration file named '%s' found.", configFile));
} else {
Scope.getCurrentScope().getLog(getClass()).info(String.format("%s configuration file located at '%s'.", configFile, resource.getUri()));
is = resource.openInputStream();
properties.load(is);
}
} catch (IOException ioe) {
throw new UnexpectedLiquibaseException(ioe);
} finally {
try {
if (is != null) {
is.close();
}
} catch (Exception ignored) {
//ignored
}
}
return properties;
}
private int determineTimeout(Properties properties) {
String timeoutString = properties.getProperty("liquibase.mongosh.timeout");
if (timeoutString == null) {
return -1;
}
try {
return Integer.parseInt(timeoutString);
} catch (Exception e) {
throw new UnexpectedLiquibaseException("Invalid value '" + timeoutString +
"' for property 'liquibase.mongosh.timeout'. Must be a valid integer. Learn more at https://docs.liquibase.com/mongodb");
}
}
private void logProperties() {
if (keepTempFile != null) {
Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a keep temp file value of '" + keepTempFile + "'");
}
if (tempPath != null) {
Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a keep temp file path value of '" + tempPath + "'");
}
if (tempName != null) {
Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a keep temp file name value of '" + tempName + "'");
}
if (logFile != null) {
Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a log file value of '" + logFile + "'");
}
}
private void handleArgs(String argsString) {
if (argsString != null) {
argsString = argsString.trim();
Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a extra arguments of '" + argsString + "'");
args = StringUtil.splitAndTrim(argsString, " ");
}
}
private void handleTimeout(Integer timeout) {
if (timeout != null) {
validateTimeout(timeout);
setTimeout(String.valueOf(timeout));
Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a timeout of '" + timeout + "'");
}
}
private void handleMongoShExecutable(File mongoshExec) {
if (mongoshExec == null) {
return;
}
if (!mongoshExec.exists()) {
throw new UnexpectedLiquibaseException("The executable for the native executor 'mongosh' cannot be found at path '" + mongoshExec.getAbsolutePath() + "' " +
"as specified in the liquibase.mongosh.conf file, the LIQUIBASE_MONGOSH_* environment variables, or other config locations. " +
"Learn more at https://docs.liquibase.com/concepts/advanced/runwith.html and https://docs.liquibase.com/mongodb");
}
if (!mongoshExec.canExecute()) {
throw new UnexpectedLiquibaseException("The 'mongosh' executable in the liquibase.mongosh.conf file at " + mongoshExec.getAbsolutePath() + " cannot be executed. Learn more at https://docs.liquibase.com/mongodb");
}
try {
setExecutable(mongoshExec.getCanonicalPath());
Scope.getCurrentScope().getLog(getClass()).info("Using the 'mongosh' executable located at: '" + mongoshExec.getCanonicalPath() + "'");
this.mongoshExec = mongoshExec;
} catch (IOException ioe) {
throw new UnexpectedLiquibaseException(ioe);
}
}
private void setupConfProperties(Properties properties) {
if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_BASE))) {
keepTempFile = getBooleanFromProperties(properties, MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_BASE));
}
if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_NAME))) {
tempName = properties.getProperty(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_NAME));
}
if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_PATH))) {
tempPath = properties.getProperty(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_PATH));
}
if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(LOG_FILE))) {
logFile = properties.getProperty(MongoshConfiguration.ConfigurationKeys.getFullKey(LOG_FILE));
}
if (properties.containsKey("liquibase.mongosh.path")) {
mongoshExec = new File(properties.getProperty("liquibase.mongosh.path"));
}
if (properties.containsKey("liquibase.mongosh.timeout")) {
timeout = determineTimeout(properties);
}
if (properties.containsKey("liquibase.mongosh.args")) {
handleArgs(properties.getProperty("liquibase.mongosh.args"));
}
}
private void assignPropertiesFromConfiguration() {
//Override .conf properties with cli/mvn/.properties properties.
keepTempFile = MongoshConfiguration.TEMP_KEEP.getCurrentValue() != null ? MongoshConfiguration.TEMP_KEEP.getCurrentValue() : keepTempFile;
tempName = MongoshConfiguration.TEMP_NAME.getCurrentValue() != null ? MongoshConfiguration.TEMP_NAME.getCurrentValue() : tempName;
tempPath = MongoshConfiguration.TEMP_PATH.getCurrentValue() != null ? MongoshConfiguration.TEMP_PATH.getCurrentValue() : tempPath;
logFile = MongoshConfiguration.LOG_FILE.getCurrentValue() != null ? MongoshConfiguration.LOG_FILE.getCurrentValue() : logFile;
timeout = MongoshConfiguration.TIMEOUT.getCurrentValue() != null ? MongoshConfiguration.TIMEOUT.getCurrentValue() : timeout;
if (MongoshConfiguration.PATH.getCurrentValue() != null) {
mongoshExec = new File(MongoshConfiguration.PATH.getCurrentValue());
}
if (MongoshConfiguration.ARGS.getCurrentValue() != null) {
handleArgs(MongoshConfiguration.ARGS.getCurrentValue());
}
}
}