001package io.ebean.docker.commands;
002
003import io.ebean.docker.container.Container;
004
005import java.sql.Connection;
006import java.sql.SQLException;
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.List;
010import java.util.Properties;
011
012/**
013 * Commands for controlling a postgres docker container.
014 * <p>
015 * References: https://github.com/docker-library/postgres/issues/146
016 */
017public class PostgresContainer extends JdbcBaseDbContainer implements Container {
018
019  /**
020   * Create Postgres container with configuration from properties.
021   */
022  public static PostgresContainer create(String pgVersion, Properties properties) {
023    return new PostgresContainer(new PostgresConfig(pgVersion, properties));
024  }
025
026  public PostgresContainer(PostgresConfig config) {
027    super(config);
028  }
029
030  @Override
031  void createDatabase() {
032    createRoleAndDatabase(false);
033  }
034
035  @Override
036  void dropCreateDatabase() {
037    createRoleAndDatabase(true);
038  }
039
040  private void createRoleAndDatabase(boolean withDrop) {
041    try (Connection connection = config.createAdminConnection()) {
042      if (withDrop) {
043        dropDatabaseIfExists(connection, dbConfig.getDbName());
044        dropRoleIfExists(connection, dbConfig.getUsername());
045      }
046      if (databaseNotExists(connection, dbConfig.getDbName())) {
047        createExtraDb(connection, withDrop);
048        createRole(connection);
049        createDatabase(connection);
050      }
051    } catch (SQLException e) {
052      throw new RuntimeException("Error when creating database and role", e);
053    }
054  }
055
056  private void dropRoleIfExists(Connection connection, String username) {
057    sqlRun(connection, "drop role if exists " + username);
058  }
059
060  private void dropDatabaseIfExists(Connection connection, String dbName) {
061    sqlRun(connection, "drop database if exists " + dbName);
062  }
063
064  private void createExtraDb(Connection connection, boolean withDrop) {
065    final String extraUser = getExtraDbUser();
066    if (defined(extraUser)) {
067      final String extraDb = dbConfig.getExtraDb();
068      if (withDrop) {
069        dropDatabaseIfExists(connection, extraDb);
070        dropRoleIfExists(connection, extraUser);
071      }
072      createRole(connection, extraUser, getWithDefault(dbConfig.getExtraDbPassword(), dbConfig.getPassword()));
073      if (databaseNotExists(connection, extraDb)) {
074        createDatabase(connection, false, extraDb, extraUser, dbConfig.getExtraDbInitSqlFile(), dbConfig.getExtraDbSeedSqlFile());
075      }
076    }
077  }
078
079  private void createDatabase(Connection connection) {
080    createDatabase(connection, true, dbConfig.getDbName(), dbConfig.getUsername(), dbConfig.getInitSqlFile(), dbConfig.getSeedSqlFile());
081  }
082
083  private void createRole(Connection connection) {
084    createRole(connection, dbConfig.getUsername(), dbConfig.getPassword());
085  }
086
087  private void createRole(Connection connection, String username, String password) {
088    if (!sqlHasRow(connection, "select rolname from pg_roles where rolname = '" + username + "'")) {
089      sqlRun(connection, "create role " + username + " password '" + password + "' login createrole");
090    }
091  }
092
093  private boolean databaseNotExists(Connection connection, String dbName) {
094    return !sqlHasRow(connection, "select 1 from pg_database where datname = '" + dbName + "'");
095  }
096
097  private void createDatabase(Connection connection, boolean withExtensions, String dbName,
098                              String owner, String initSql, String seedSql) {
099
100    sqlRun(connection, "create database " + dbName + " with owner " + owner);
101    if (withExtensions) {
102      addExtensions();
103    }
104    if (defined(initSql)) {
105      runDbSqlFile(dbName, owner, initSql);
106    }
107    if (defined(seedSql)) {
108      runDbSqlFile(dbName, owner, seedSql);
109    }
110  }
111
112  private void addExtensions() {
113    if (!defined(dbConfig.getExtensions())) {
114      return;
115    }
116    final List<String> extensions = parseExtensions(dbConfig.getExtensions());
117    if (!extensions.isEmpty()) {
118      try (Connection connection = dbConfig.createAdminConnection(dbConfig.jdbcUrl())) {
119        for (String extension : extensions) {
120          sqlRun(connection, "create extension if not exists \"" + extension + "\"");
121        }
122      } catch (SQLException e) {
123        throw new RuntimeException(e);
124      }
125    }
126  }
127
128  /**
129   * Maybe return an extra user to create.
130   * <p>
131   * The extra user will default to be the same as the extraDB if that is defined.
132   * Additionally we don't create an extra user IF it is the same as the main db user.
133   */
134  private String getExtraDbUser() {
135    String extraUser = getWithDefault(dbConfig.getExtraDbUser(), dbConfig.getExtraDb());
136    return extraUser != null && !extraUser.equals(dbConfig.getUsername()) ? extraUser : null;
137  }
138
139  @Override
140  protected void executeSqlFile(String dbUser, String dbName, String containerFilePath) {
141    ProcessBuilder pb = sqlFileProcess(dbUser, dbName, containerFilePath);
142    executeWithout("ERROR", pb, "Error executing init sql file: " + containerFilePath);
143  }
144
145  private ProcessBuilder sqlFileProcess(String dbUser, String dbName, String containerFilePath) {
146    List<String> args = execPsql();
147    args.add(dbUser);
148    args.add("-d");
149    args.add(dbName);
150    args.add("-f");
151    args.add(containerFilePath);
152    return createProcessBuilder(args);
153  }
154
155  private String getWithDefault(String value, String defaultValue) {
156    return value == null ? defaultValue : value;
157  }
158
159  private List<String> parseExtensions(String dbExtn) {
160    if (dbExtn == null) {
161      return Collections.emptyList();
162    }
163    List<String> extensions = new ArrayList<>();
164    for (String extension : dbExtn.split(",")) {
165      extension = extension.trim();
166      if (!extension.isEmpty()) {
167        extensions.add(extension);
168      }
169    }
170    return extensions;
171  }
172
173  private List<String> execPsql() {
174    List<String> args = new ArrayList<>();
175    args.add(config.docker);
176    args.add("exec");
177    args.add("-i");
178    args.add(config.containerName());
179    args.add("psql");
180    args.add("-U");
181    return args;
182  }
183
184  @Override
185  protected ProcessBuilder runProcess() {
186    List<String> args = dockerRun();
187    if (dbConfig.isInMemory() && dbConfig.getTmpfs() != null) {
188      args.add("--tmpfs");
189      args.add(dbConfig.getTmpfs());
190    }
191    if (!dbConfig.adminPassword.isEmpty()) {
192      args.add("-e");
193      args.add("POSTGRES_PASSWORD=" + dbConfig.getAdminPassword());
194    }
195    args.add(config.getImage());
196    return createProcessBuilder(args);
197  }
198
199}