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}