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}