Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
au.net.causal.maven.plugins.boxdb.db.SqlServerLinuxDatabase Maven / Gradle / Ivy
package au.net.causal.maven.plugins.boxdb.db;
import au.net.causal.maven.plugins.boxdb.ImageCheckerUtils;
import au.net.causal.maven.plugins.boxdb.JdbcSqlRunner;
import au.net.causal.maven.plugins.boxdb.ScriptReaderRunner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.fabric8.maven.docker.access.DockerAccess;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.config.LogConfiguration;
import io.fabric8.maven.docker.config.RunImageConfiguration;
import io.fabric8.maven.docker.config.RunVolumeConfiguration;
import io.fabric8.maven.docker.log.LogOutputSpec;
import org.apache.maven.plugin.MojoExecutionException;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SqlServerLinuxDatabase extends DockerDatabase
{
private final Path hostBackupDirectory;
private final Path hostScriptDirectory;
public SqlServerLinuxDatabase(BoxConfiguration boxConfiguration, ProjectConfiguration projectConfiguration,
BoxContext context, DockerRegistry dockerRegistry)
throws IOException
{
super(boxConfiguration, projectConfiguration, context, dockerRegistry);
//Generate host script directory that is used to give scripts to SQL Server
hostScriptDirectory = context.getTempDirectory().resolve(containerName() + "-scripts");
if (Files.notExists(hostScriptDirectory))
Files.createDirectories(hostScriptDirectory);
hostBackupDirectory = context.getTempDirectory().resolve(containerName() + "-backups");
if (Files.notExists(hostBackupDirectory))
Files.createDirectories(hostBackupDirectory);
}
@Override
protected int containerDatabasePort()
{
return 1433;
}
@Override
protected String dockerImageName()
{
//This -ubuntu suffix on databaseVersion also affects version listing in SqlServerLinuxFactory#availableVersions()
//and checkImage()
return "mcr.microsoft.com/mssql/server:" + getBoxConfiguration().getDatabaseVersion() + "-ubuntu";
}
@Override
protected void configureRunImage(RunImageConfiguration.Builder builder)
{
super.configureRunImage(builder);
RunVolumeConfiguration backupVolume = new RunVolumeConfiguration.Builder()
.bind(Arrays.asList(
getBackupDirectory().toAbsolutePath().toString() + ":/data/backup",
getScriptDirectory().toAbsolutePath().toString() + ":/data/scripts"))
.build();
builder.env(ImmutableMap.of("SA_PASSWORD", getBoxConfiguration().getAdminPassword(),
"ACCEPT_EULA", "Y"));
builder.volumes(backupVolume);
}
@Override
public JdbcConnectionInfo jdbcConnectionInfo(DatabaseTarget target)
throws BoxDatabaseException
{
return driverType().jdbcConnectionInfo(target, getBoxConfiguration(), getContext().getDockerHostAddress());
}
protected SqlServerJdbcDriverType driverType()
{
return SqlServerJdbcDriverType.fromBoxConfiguration(getBoxConfiguration());
}
@Override
public JdbcDriverInfo jdbcDriverInfo() throws BoxDatabaseException
{
return driverType().jdbcDriver();
}
protected DataSourceBuilder dataSourceBuilder(DatabaseTarget target)
throws BoxDatabaseException
{
DataSourceBuilder builder = new DataSourceBuilder(getContext());
driverType().configureDataSourceBuilder(builder, target, getBoxConfiguration(), getContext().getDockerHostAddress());
return builder;
}
@Override
public Connection createJdbcConnection(DatabaseTarget targetDatabase)
throws SQLException, BoxDatabaseException, IOException
{
configureDriverJdbcLogging();
return dataSourceBuilder(targetDatabase).create().getConnection();
}
/**
* Configures logging for the Microsoft JDBC driver. Logging is turned down so that connection errors occurring
* during JDBC waiting don't spam the logs.
*/
protected void configureDriverJdbcLogging()
{
//I know this is static and only needed to be done once as opposed to on every connection creation but this way
//it's only done when needed and doing it multiple times is harmless
if (driverType() == SqlServerJdbcDriverType.MICROSOFT)
{
Logger connectionlogger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerConnection");
connectionlogger.setLevel(Level.SEVERE);
}
}
@Override
public void executeJdbcScript(Reader scriptReader, DatabaseTarget targetDatabase)
throws IOException, SQLException, BoxDatabaseException
{
try (Connection con = createJdbcConnection(targetDatabase);
JdbcSqlRunner sqlRunner = new JdbcSqlRunner(con, getContext().getLog()))
{
sqlRunner.executeSql(new BufferedReader(scriptReader));
}
}
@Override
public void configureNewDatabase()
throws IOException, SQLException, BoxDatabaseException
{
runFilteredScript("sqlserver-linux-create-database.sql");
}
private void runFilteredScript(String scriptResourceName)
throws IOException, BoxDatabaseException, SQLException
{
URL scriptResource = SqlServerLinuxDatabase.class.getResource(scriptResourceName);
if (scriptResource == null)
throw new FileNotFoundException("Missing script resource: " + scriptResourceName);
ScriptReaderRunner scriptRunner = getContext().createScriptReaderRunner(this, getBoxConfiguration(), getProjectConfiguration());
ScriptReaderExecution execution = new ScriptReaderExecution();
execution.setFiltering(true);
execution.setScripts(Arrays.asList(scriptResource));
try
{
scriptRunner.execute(execution, DatabaseTarget.ADMIN, getProjectConfiguration().getScriptTimeout());
}
catch (MojoExecutionException e)
{
throw new BoxDatabaseException(e);
}
}
@Override
protected void executeScriptFile(Path scriptFile, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SQLException, BoxDatabaseException
{
if (!Files.exists(scriptFile))
throw new NoSuchFileException(scriptFile.toString());
//Copy the script file to inside our script dir mounted to the container
Path mountedScriptFile = Files.createTempFile(hostScriptDirectory, "script", ".sql");
Files.copy(scriptFile, mountedScriptFile, StandardCopyOption.REPLACE_EXISTING);
executeSqlCmd("-i /data/scripts/" + mountedScriptFile.getFileName().toString(), targetDatabase, timeout);
}
protected String sqlToolsPath()
{
return "/opt/mssql-tools/bin/";
}
private void executeSqlCmd(String args, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SqlCmdException, BoxDatabaseException
{
//Docker plugin provides no easy way to get exit code from exec call so we need to script it and
//return its value via the file system and a temp file
Path returnCodeFile = Files.createTempFile(hostScriptDirectory, "return", ".txt");
executeSqlCmd(args, targetDatabase, timeout, returnCodeFile, false);
}
private void executeSqlCmd(String args, DatabaseTarget targetDatabase, Duration timeout, Path returnCodeFile, boolean silent)
throws IOException, SqlCmdException, BoxDatabaseException
{
String dbName = (targetDatabase == DatabaseTarget.ADMIN ? "master" : getBoxConfiguration().getDatabaseName());
args = "-H localhost -b -e -U " + targetDatabase.user(getBoxConfiguration()) + " -P " + targetDatabase.password(getBoxConfiguration()) +
" -d " + dbName + " " + args;
DockerAccess docker = getContext().getDockerServiceHub().getDockerAccess();
LogConfiguration logConfig = new LogConfiguration.Builder()
.enabled(!silent)
.prefix("sqlcmd")
.build();
RunImageConfiguration sqlCmdRunConfig = new RunImageConfiguration.Builder()
.links(Collections.singletonList(containerName() + ":sqlcmd"))
.containerNamePattern("%a")
.cmd("unused") //not used but must be non-null - is replaced later
.log(logConfig)
.build();
String sqlCmdExecutablePath = sqlToolsPath() + "sqlcmd";
sqlCmdRunConfig.getCmd().setExec(Arrays.asList("bash", "-i", "-c", sqlCmdExecutablePath + " " + args + " ; echo $? > " + "/data/scripts/" + returnCodeFile.getFileName().toString()));
getContext().getLog().debug("Executing command: " + sqlCmdRunConfig.getCmd().getExec());
sqlCmdRunConfig.getCmd().setShell(null);
ImageConfiguration sqlPlusConfig = new ImageConfiguration.Builder()
.runConfig(sqlCmdRunConfig)
.name(dockerImageName())
.alias(containerName() + "-sqlcmd")
.build();
String containerId = findDockerContainer().getId();
String execContainerId = docker.createExecContainer(containerId, sqlCmdRunConfig.getCmd());
LogOutputSpec execLog;
Path logFile = null;
if (silent)
{
logFile = Files.createTempFile(returnCodeFile.getParent(), "wait", ".log");
execLog = new LogOutputSpec.Builder().logStdout(false).file(logFile.toAbsolutePath().toString()).build();
}
else
execLog = getContext().getLogSpecFactory().createSpec(execContainerId, sqlPlusConfig);
getContext().getLog().debug("Beginning exec container " + execContainerId);
docker.startExecContainer(execContainerId, execLog);
getContext().getLog().debug("Exec container done " + execContainerId);
String exitCode = new String(Files.readAllBytes(returnCodeFile), StandardCharsets.UTF_8).trim();
getContext().getLog().debug("All done: " + exitCode);
//If we wanted silent run we won't want to keep the output so just delete these files
//otherwise we'll have a huge amount of them, especially for the waiter script
if (logFile != null)
{
if (getContext().getLog().isDebugEnabled())
getContext().getLog().debug(Files.lines(logFile).collect(Collectors.joining(System.lineSeparator())));
try
{
Files.deleteIfExists(logFile);
}
catch (IOException e)
{
//Not a huge deal if we can't delete
getContext().getLog().debug("Failed to delete temporary log file " + logFile + ": " + e, e);
}
}
if (!"0".equals(exitCode))
throw new SqlCmdException("sqlcmd exit code: " + exitCode, exitCode);
}
@Override
public void executeSql(String sql, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SQLException, BoxDatabaseException
{
//TODO could do this directly instead of writing to file first
try (StringReader reader = new StringReader(sql))
{
executeScript(reader, targetDatabase, timeout);
}
}
private Path getBackupDirectory()
{
return hostBackupDirectory;
}
private Path getScriptDirectory()
{
return hostScriptDirectory;
}
@Override
public void backup(Path backupFile, BackupFileTypeHint backupFileTypeHint)
throws BoxDatabaseException, IOException, SQLException
{
Path targetFile = getBackupDirectory().resolve("backup.bak");
//Backup to backup.bak file
String sql = "backup database " + getBoxConfiguration().getDatabaseName() + " to disk = '/data/backuplocal/backup.bak' with format;";
executeJdbcSqlCommand(sql, DatabaseTarget.ADMIN);
//Copy backup to guest's backup directory
doFileCopy("/data/backuplocal/backup.bak", "/data/backup/backup.bak");
//Move file to destination
Files.move(targetFile, backupFile, StandardCopyOption.REPLACE_EXISTING);
}
@Override
public void restore(Path backupFile) throws BoxDatabaseException, IOException, SQLException
{
Path targetFile = getBackupDirectory().resolve("backup.bak");
//Copy backup file to vagrant directory
Files.copy(backupFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
//Copy host to guest backup directory
doFileCopy("/data/backup/backup.bak", "/data/backuplocal/backup.bak");
executeRestoreSqlCmd("/data/backuplocal/backup.bak");
}
@Override
public void restore(URL backupResource)
throws BoxDatabaseException, IOException, SQLException
{
Path targetFile = getBackupDirectory().resolve("backup.bak");
//Copy backup file to vagrant directory
try (InputStream is = backupResource.openStream())
{
Files.copy(is, targetFile, StandardCopyOption.REPLACE_EXISTING);
}
//Copy host to guest backup directory
doFileCopy("/data/backup/backup.bak", "/data/backuplocal/backup.bak");
executeRestoreSqlCmd("/data/backuplocal/backup.bak");
}
private static String parentDirectoryOfPath(String path)
{
List segments = Arrays.asList(path.split(Pattern.quote("/")));
return String.join("/", segments.subList(0, segments.size() - 1));
}
private void doFileCopy(String fromContainerPath, String toContainerPath)
throws IOException, BoxDatabaseException
{
String toContainerDirectoryPath = parentDirectoryOfPath(toContainerPath);
DockerAccess docker = getContext().getDockerServiceHub().getDockerAccess();
LogConfiguration logConfig = new LogConfiguration.Builder()
.enabled(true)
.prefix("cp")
.build();
RunImageConfiguration cpRunConfig = new RunImageConfiguration.Builder()
.links(Collections.singletonList(containerName() + ":sqlserver"))
.containerNamePattern("%a")
.cmd("unused") //not used but must be non-null - is replaced later
.log(logConfig)
.build();
//The chmod is necessary for SQL server because by default backups are non-readable by everyone and owned by root
cpRunConfig.getCmd().setExec(Arrays.asList("bash", "-i", "-c", "mkdir -p " + toContainerDirectoryPath + " ; cp " + fromContainerPath + " " + toContainerPath + " ; chmod 0644 " + toContainerPath));
getContext().getLog().debug("Executing command: " + cpRunConfig.getCmd().getExec());
cpRunConfig.getCmd().setShell(null);
ImageConfiguration cpConfig = new ImageConfiguration.Builder()
.runConfig(cpRunConfig)
.name(dockerImageName())
.alias(containerName() + "-cp")
.build();
String containerId = findDockerContainer().getId();
String execContainerId = docker.createExecContainer(containerId, cpRunConfig.getCmd());
LogOutputSpec execLog = getContext().getLogSpecFactory().createSpec(execContainerId, cpConfig);
docker.startExecContainer(execContainerId, execLog);
getContext().getLog().debug("Exec container done " + execContainerId);
}
private void executeJdbcSqlCommand(String sql, DatabaseTarget target)
throws SQLException, BoxDatabaseException, IOException
{
try (StringReader reader = new StringReader(sql))
{
executeJdbcScript(reader, target);
}
}
private void executeRestoreSqlCmd(String backupFilePath)
throws SQLException, BoxDatabaseException, IOException
{
String dropSql = "drop database if exists " + getBoxConfiguration().getDatabaseName() + ";";
executeJdbcSqlCommand(dropSql, DatabaseTarget.ADMIN);
String sql = "restore database " + getBoxConfiguration().getDatabaseName() + " from disk = '" + backupFilePath + "' with replace;";
executeJdbcSqlCommand(sql, DatabaseTarget.ADMIN);
runFilteredScript("sqlserver-linux-post-restore-users.sql");
}
@Override
public void waitUntilStarted(Duration maxTimeToWait) throws TimeoutException, BoxDatabaseException
{
long startTime = System.currentTimeMillis();
long maxEndTime = startTime + maxTimeToWait.toMillis();
super.waitUntilStarted(maxTimeToWait);
//Use JDBC waiting to fully wait until database is up
//Sadly it's our only option for SQL Server for Linux at the moment
//since there is a window where docker returns and the database is not fully started and will refuse login
boolean connectionSucceeded = false;
while (!connectionSucceeded && System.currentTimeMillis() < maxEndTime)
{
try (Connection con = createJdbcConnection(DatabaseTarget.ADMIN))
{
connectionSucceeded = true;
}
catch (SQLException e)
{
//A subclass might override this for some reason so let's support it anyway - by default returns false
if (databaseIsReady(e))
{
connectionSucceeded = true;
getContext().getLog().debug("Failed to connect, but database deemed it is ready: " + e, e);
}
else if (isLoginFailureBeforeDatabaseFullyStarted(e) || isConnectionFailureToNotYetStartedDatabase(e))
{
//Happened because database not fully started
getContext().getLog().debug("Loop waiting for database connectivity failed to get connection", e);
try
{
Thread.sleep(getProjectConfiguration().getPollTime().toMillis());
}
catch (InterruptedException ex)
{
throw new BoxDatabaseException("Interrupted waiting for database to start up: " + ex, ex);
}
}
else
throw new BoxDatabaseException("Error occurred connecting to database to verify startup: " + e, e);
}
catch (IOException e)
{
throw new BoxDatabaseException("I/O error occured waiting for database to start up: " + e, e);
}
}
if (!connectionSucceeded)
throw new TimeoutException("Timed out waiting for database to come up.");
}
private boolean isLoginFailureBeforeDatabaseFullyStarted(SQLException ex)
{
switch (driverType())
{
case MICROSOFT:
return "S0001".equals(ex.getSQLState()) && ex.getErrorCode() == 18456;
case JTDS:
return "28000".equals(ex.getSQLState()) && ex.getErrorCode() == 18456;
default:
throw new Error("Unknown driver type: " + driverType());
}
}
private boolean isConnectionFailureToNotYetStartedDatabase(SQLException ex)
{
switch (driverType())
{
//Microsoft driver handles connection re-attempts itself but its timeout it sometimes not high enough,
//so we'll handle it here as well and allow retrying
//Both JTDS and Microsoft driver have same state for when pre-login connection fails
case MICROSOFT:
case JTDS:
return "08S01".equals(ex.getSQLState());
default:
throw new Error("Unknown driver type: " + driverType());
}
}
@Override
public Collection extends ImageComponent> checkImage()
throws BoxDatabaseException
{
ImageComponent jdbcDriverComponent = ImageCheckerUtils.checkImageUsingMavenDependencies("JDBC driver",
getContext(),
jdbcDriverInfo().getDependencies());
//-ubuntu suffix is special for SQL Server
ImageComponent dockerDatabaseComponent = checkDockerDatabaseImage(SqlServerLinuxFactory.SQLSERVER_DOCKER_REPOSITORY,
getBoxConfiguration().getDatabaseVersion() + "-ubuntu");
return ImmutableList.of(jdbcDriverComponent, dockerDatabaseComponent);
}
public static class SqlCmdException extends SQLException
{
private final String exitCode;
public SqlCmdException(String reason, String exitCode)
{
super(reason);
this.exitCode = exitCode;
}
public String getExitCode()
{
return exitCode;
}
}
}