001package io.ebean.docker.commands; 002 003import java.sql.Connection; 004import java.sql.PreparedStatement; 005import java.sql.ResultSet; 006import java.sql.SQLException; 007import java.sql.Statement; 008import java.util.ArrayList; 009import java.util.List; 010import java.util.Properties; 011import java.util.function.Consumer; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import io.ebean.docker.container.Container; 017 018/** 019 * Commands for controlling a SAP HANA docker container. 020 */ 021public class HanaContainer extends DbContainer implements Container { 022 023 /** 024 * Create SAP HANA container with configuration from properties. 025 */ 026 public static HanaContainer create(String version, Properties properties) { 027 return new HanaContainer(new HanaConfig(version, properties)); 028 } 029 030 private static final Logger log = LoggerFactory.getLogger(Commands.class); 031 032 private final HanaConfig hanaConfig; 033 034 /** 035 * Create with configuration. 036 */ 037 public HanaContainer(HanaConfig config) { 038 super(config); 039 this.hanaConfig = config; 040 String osName = System.getProperty("os.name").toLowerCase(); 041 if (!osName.contains("linux")) { 042 throw new IllegalStateException("The HANA docker image requires a Linux operating system"); 043 } 044 if (!hanaConfig.isAgreeToSapLicense()) { 045 throw new IllegalStateException( 046 "You must agree to the SAP license (https://www.sap.com/docs/download/cmp/2016/06/sap-hana-express-dev-agmt-and-exhibit.pdf) by setting the property 'hana.agreeToSapLicense' to 'true'"); 047 } 048 } 049 050 @Override 051 protected boolean isDatabaseAdminReady() { 052 return isDatabaseReady(); 053 } 054 055 @Override 056 protected boolean isDatabaseReady() { 057 return commands.logsContain(config.containerName(), "Startup finished!"); 058 } 059 060 /** 061 * Start the container and wait for it to be ready. 062 * <p> 063 * This checks if the container is already running. 064 * </p> 065 * <p> 066 * Returns false if the wait for ready was unsuccessful. 067 * </p> 068 */ 069 @Override 070 public boolean startWithCreate() { 071 startIfNeeded(); 072 if (!waitForDatabaseReady()) { 073 log.warn("Failed waitForDatabaseReady for container {}", config.containerName()); 074 return false; 075 } 076 if (!createUserIfNotExists()) { 077 return false; 078 } 079 if (!waitForConnectivity()) { 080 log.warn("Failed waiting for connectivity"); 081 return false; 082 } 083 return true; 084 } 085 086 /** 087 * Start with a drop and create of the database and user. 088 */ 089 @Override 090 public boolean startWithDropCreate() { 091 startIfNeeded(); 092 if (!waitForDatabaseReady()) { 093 log.warn("Failed waitForDatabaseReady for container {}", config.containerName()); 094 return false; 095 } 096 097 dropUserIfExists(); 098 099 if (!createUserIfNotExists()) { 100 return false; 101 } 102 if (!waitForConnectivity()) { 103 log.warn("Failed waiting for connectivity"); 104 return false; 105 } 106 return true; 107 } 108 109 @Override 110 protected ProcessBuilder runProcess() { 111 112 List<String> args = new ArrayList<>(); 113 args.add(config.docker); 114 args.add("run"); 115 args.add("-d"); 116 args.add("-p"); 117 args.add("3" + hanaConfig.getInstanceNumber() + "13:39013"); 118 args.add("-p"); 119 args.add(config.getPort() + ":" + config.getInternalPort()); 120 args.add("-p"); 121 args.add("3" + hanaConfig.getInstanceNumber() + "41-3" + hanaConfig.getInstanceNumber() + "45:39041-39045"); 122 args.add("-v"); 123 args.add(hanaConfig.getMountsDirectory() + ":/hana/mounts"); 124 args.add("--ulimit"); 125 args.add("nofile=1048576:1048576"); 126 args.add("--sysctl"); 127 args.add("kernel.shmmax=1073741824"); 128 args.add("--sysctl"); 129 args.add("kernel.shmmni=524288"); 130 args.add("--sysctl"); 131 args.add("kernel.shmall=8388608"); 132 args.add("--name"); 133 args.add(config.containerName()); 134 args.add(config.getImage()); 135 args.add("--passwords-url"); 136 args.add(hanaConfig.getPasswordsUrl().toString()); 137 if (hanaConfig.isAgreeToSapLicense()) { 138 args.add("--agree-to-sap-license"); 139 } 140 141 return createProcessBuilder(args); 142 } 143 144 private boolean dropUserIfExists() { 145 log.info("Drop database user {} if exists", dbConfig.getUsername()); 146 sqlProcess(connection -> { 147 if (userExists(connection)) { 148 sqlRun(connection, "drop user " + dbConfig.getUsername() + " cascade"); 149 } 150 }); 151 return true; 152 } 153 154 private boolean createUserIfNotExists() { 155 log.info("Create database user {} if not exists", dbConfig.getUsername()); 156 sqlProcess(connection -> { 157 if (!userExists(connection)) { 158 sqlRun(connection, "create user " + dbConfig.getUsername() + " password " + dbConfig.getPassword() 159 + " no force_first_password_change"); 160 } 161 }); 162 return true; 163 } 164 165 private boolean userExists(Connection connection) { 166 try (PreparedStatement statement = connection 167 .prepareStatement("select count(*) from sys.users where user_name = upper(?)")) { 168 statement.setString(1, dbConfig.getUsername()); 169 try (ResultSet rs = statement.executeQuery()) { 170 if (rs.next()) { 171 int count = rs.getInt(1); 172 return count == 1; 173 } 174 return false; 175 } 176 177 } catch (SQLException e) { 178 log.error("Failed to execute sql to check if user exists", e); 179 return false; 180 } 181 } 182 183 @SuppressWarnings("unchecked") 184 private <E extends Throwable> void sneakyThrow(Throwable t) throws E { 185 throw (E) t; 186 } 187}