Browse Source

added master/slave modes; added migrate.py

tags/9.6-1.1.0
Robin Thoni 6 years ago
parent
commit
1882875b27

+ 18
- 5
Dockerfile View File

@@ -1,19 +1,32 @@
1 1
 FROM robinthoni/postgres-multiarch:9.6
2 2
 
3
+RUN mv -v /usr/share/postgresql/$PG_MAJOR/pg_hba.conf.sample /usr/share/postgresql/ \
4
+              && ln -sv ../pg_hba.conf.sample /usr/share/postgresql/$PG_MAJOR/
5
+
3 6
 RUN apt-get update && apt-get -y install\
4
-        cron &&\
7
+        cron\
8
+        python3\
9
+        python3-pip\
10
+        &&\
5 11
         apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
6 12
 
13
+RUN pip3 install -U\
14
+      argparse
15
+
16
+RUN mkdir /docker-entrypoint-initdb-core.d
17
+
18
+COPY ./docker-entrypoint-initdb-core.d/ /docker-entrypoint-initdb-core.d
19
+
7 20
 COPY ./backup/pg_backup.config /etc/pg_backup.config
8 21
 
9 22
 COPY ./backup/pg_backup_rotated.sh /usr/local/bin/pg_backup_rotated.sh
10 23
 
11 24
 COPY ./backup/pg_backup_rotated.cron.sh /etc/cron.daily/pg_backup_rotated
12 25
 
13
-COPY ./run.sh /run.sh
26
+COPY ./docker-entrypoint.sh /usr/local/bin/
14 27
 
15
-VOLUME "/var/lib/postgresql/backup"
28
+COPY ./migrate.py /usr/local/bin/
16 29
 
17
-ENTRYPOINT ["/run.sh"]
30
+RUN rm -rf /var/log/*
18 31
 
19
-CMD ["postgres"]
32
+VOLUME "/var/lib/postgresql/backup"

+ 4
- 0
backup/pg_backup_rotated.cron.sh View File

@@ -1,3 +1,7 @@
1 1
 #! /usr/bin/env bash
2 2
 
3
+if [ "${POSTGRES_NO_BACKUP}" == 1 ]
4
+then
5
+  exit 0
6
+fi
3 7
 pg_backup_rotated.sh -c /etc/pg_backup.config > /dev/null

+ 18
- 0
docker-entrypoint-initdb-core.d/1.0.0_01_create_db.sh View File

@@ -0,0 +1,18 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_SLAVE_MODE}" == 1 ]
4
+then
5
+  echo "Database is in slave mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+if [ "${POSTGRES_USER}" != 'postgres' ]
10
+then
11
+  psql <<-EOF
12
+  CREATE USER "${POSTGRES_USER}" WITH SUPERUSER PASSWORD '${POSTGRES_PASSWORD}';
13
+EOF
14
+else
15
+  psql <<-EOF
16
+  ALTER USER "${POSTGRES_USER}" WITH SUPERUSER PASSWORD '${POSTGRES_PASSWORD}';
17
+EOF
18
+fi

+ 14
- 0
docker-entrypoint-initdb-core.d/1.0.0_02_create_user.sh View File

@@ -0,0 +1,14 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_SLAVE_MODE}" == 1 ]
4
+then
5
+  echo "Database is in slave mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+if [ "${POSTGRES_DB}" != 'postgres' ]
10
+then
11
+  psql <<-EOF
12
+  CREATE DATABASE "${POSTGRES_DB}";
13
+EOF
14
+fi

+ 11
- 0
docker-entrypoint-initdb-core.d/1.1.0_01_master_create_rep_user.sh View File

@@ -0,0 +1,11 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_MASTER_MODE}" != 1 ]
4
+then
5
+  echo "Database is not in master mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+psql <<-EOF
10
+  CREATE USER "${POSTGRES_REP_USER}" REPLICATION LOGIN ENCRYPTED PASSWORD '${POSTGRES_REP_PASSWORD}';
11
+EOF

+ 25
- 0
docker-entrypoint-initdb-core.d/1.1.0_02_master_create_ro_user.sh View File

@@ -0,0 +1,25 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_MASTER_MODE}" != 1 ]
4
+then
5
+  echo "Database is not in master mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+psql <<-EOF
10
+  CREATE USER "${POSTGRES_RO_USER}"
11
+    NOSUPERUSER
12
+    NOCREATEDB
13
+    NOCREATEROLE
14
+    LOGIN
15
+    ENCRYPTED PASSWORD '${POSTGRES_RO_PASSWORD}';
16
+
17
+  REVOKE ALL ON DATABASE ${POSTGRES_DB} FROM ${POSTGRES_RO_USER};
18
+  GRANT CONNECT ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_RO_USER};
19
+
20
+  REVOKE ALL ON SCHEMA public FROM ${POSTGRES_RO_USER};
21
+  GRANT USAGE ON SCHEMA public TO ${POSTGRES_RO_USER};
22
+
23
+  GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${POSTGRES_RO_USER};
24
+  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${POSTGRES_RO_USER};
25
+EOF

+ 18
- 0
docker-entrypoint-initdb-core.d/1.1.0_03_master_edit_postgres_conf.sh View File

@@ -0,0 +1,18 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_MASTER_MODE}" != 1 ]
4
+then
5
+  echo "Database is not in master mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+replace_pg_conf()
10
+{
11
+  var_name="${1}"
12
+  var_value="${2}"
13
+  sed -ri "s!^#?(${var_name})\s*=\s*\S+.*!\1 = ${var_value}!" /var/lib/postgresql/data/postgresql.conf
14
+}
15
+
16
+replace_pg_conf wal_level replica
17
+replace_pg_conf max_wal_senders 3
18
+replace_pg_conf wal_keep_segments 8

+ 11
- 0
docker-entrypoint-initdb-core.d/1.1.0_04_master_edit_pg_hba_conf.sh View File

@@ -0,0 +1,11 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_MASTER_MODE}" != 1 ]
4
+then
5
+  echo "Database is not in master mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+cat <<-EOF >> /var/lib/postgresql/data/pg_hba.conf
10
+  host    replication     ${POSTGRES_REP_USER}      ${POSTGRES_REP_ALLOWED_HOST}           md5
11
+EOF

+ 19
- 0
docker-entrypoint-initdb-core.d/1.1.0_05_slave_edit_postgres_conf.sh View File

@@ -0,0 +1,19 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_SLAVE_MODE}" != 1 ]
4
+then
5
+  echo "Database is not in slave mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+replace_pg_conf()
10
+{
11
+  var_name="${1}"
12
+  var_value="${2}"
13
+  sed -ri "s!^#?(${var_name})\s*=\s*\S+.*!\1 = ${var_value}!" /var/lib/postgresql/data/postgresql.conf
14
+}
15
+
16
+replace_pg_conf wal_level replica
17
+replace_pg_conf max_wal_senders 3
18
+replace_pg_conf wal_keep_segments 8
19
+replace_pg_conf hot_standby on

+ 14
- 0
docker-entrypoint-initdb-core.d/1.1.0_06_slave_edit_recovery_conf.sh View File

@@ -0,0 +1,14 @@
1
+#! /usr/bin/env bash
2
+
3
+if [ "${POSTGRES_SLAVE_MODE}" != 1 ]
4
+then
5
+  echo "Database is not in slave mode. Exiting."
6
+  exit 0
7
+fi
8
+
9
+
10
+cat <<-EOF > "${PGDATA}/recovery.conf"
11
+  standby_mode = 'on'
12
+  primary_conninfo = 'host=${POSTGRES_REP_HOST} port=${POSTGRES_REP_PORT} user=${POSTGRES_REP_USER} password=${POSTGRES_REP_PASSWORD}'
13
+  trigger_file = '/tmp/postgresql.trigger'
14
+EOF

+ 186
- 0
docker-entrypoint.sh View File

@@ -0,0 +1,186 @@
1
+#!/usr/bin/env bash
2
+set -e
3
+
4
+if [ "${POSTGRES_INDEX}" != "" ]
5
+then
6
+  set_var_value()
7
+  {
8
+    var_name="${1}"_"${POSTGRES_INDEX}"
9
+    export "${1}"="${!var_name}"
10
+  }
11
+
12
+  set_var_value POSTGRES_DISABLED
13
+  set_var_value POSTGRES_SLAVE_MODE
14
+  set_var_value POSTGRES_MASTER_MODE
15
+  set_var_value POSTGRES_NO_BACKUP
16
+  set_var_value POSTGRES_HOST
17
+  set_var_value POSTGRES_USER
18
+  set_var_value POSTGRES_PASSWORD
19
+  set_var_value POSTGRES_DB
20
+  set_var_value POSTGRES_REP_HOST
21
+  set_var_value POSTGRES_REP_PORT
22
+  set_var_value POSTGRES_REP_USER
23
+  set_var_value POSTGRES_REP_PASSWORD
24
+  set_var_value POSTGRES_REP_ALLOWED_HOST
25
+  set_var_value POSTGRES_RO_USER
26
+  set_var_value POSTGRES_RO_PASSWORD
27
+fi
28
+
29
+if [ "${POSTGRES_DISABLED}" == 1 ]
30
+then
31
+  echo "Database ${POSTGRES_INDEX} is disabled. Exiting."
32
+  exit 0
33
+fi
34
+
35
+if [ "${POSTGRES_SLAVE_MODE}" == 1 ] && [ "${POSTGRES_MASTER_MODE}" == 1 ]
36
+then
37
+  echo "Misconfiguration: master and slave mode are both enabled. Exiting."
38
+  exit 64
39
+fi
40
+
41
+if [ "$(id -u)" == "0" ]
42
+then
43
+  cron
44
+fi
45
+
46
+# usage: file_env VAR [DEFAULT]
47
+#    ie: file_env 'XYZ_DB_PASSWORD' 'example'
48
+# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
49
+#  "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
50
+file_env() {
51
+	local var="$1"
52
+	local fileVar="${var}_FILE"
53
+	local def="${2:-}"
54
+	if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
55
+		echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
56
+		exit 1
57
+	fi
58
+	local val="$def"
59
+	if [ "${!var:-}" ]; then
60
+		val="${!var}"
61
+	elif [ "${!fileVar:-}" ]; then
62
+		val="$(< "${!fileVar}")"
63
+	fi
64
+	export "$var"="$val"
65
+	unset "$fileVar"
66
+}
67
+
68
+if [ "${1:0:1}" = '-' ]; then
69
+	set -- postgres "$@"
70
+fi
71
+
72
+# allow the container to be started with `--user`
73
+if [ "$1" = 'postgres' ] && [ "$(id -u)" = '0' ]; then
74
+	mkdir -p "$PGDATA"
75
+	chown -R postgres "$PGDATA"
76
+	chmod 700 "$PGDATA"
77
+
78
+	mkdir -p /var/run/postgresql
79
+	chown -R postgres /var/run/postgresql
80
+	chmod 775 /var/run/postgresql
81
+
82
+	# Create the transaction log directory before initdb is run (below) so the directory is owned by the correct user
83
+	if [ "$POSTGRES_INITDB_XLOGDIR" ]; then
84
+		mkdir -p "$POSTGRES_INITDB_XLOGDIR"
85
+		chown -R postgres "$POSTGRES_INITDB_XLOGDIR"
86
+		chmod 700 "$POSTGRES_INITDB_XLOGDIR"
87
+	fi
88
+
89
+	exec gosu postgres "$BASH_SOURCE" "$@"
90
+fi
91
+
92
+if [ "$1" = 'postgres' ]; then
93
+	mkdir -p "$PGDATA"
94
+	chown -R "$(id -u)" "$PGDATA" 2>/dev/null || :
95
+	chmod 700 "$PGDATA" 2>/dev/null || :
96
+
97
+        if [ "${POSTGRES_SLAVE_MODE}" != 1 ]
98
+        then
99
+
100
+	    # look specifically for PG_VERSION, as it is expected in the DB dir
101
+	    if [ ! -s "$PGDATA/PG_VERSION" ]; then
102
+		file_env 'POSTGRES_INITDB_ARGS'
103
+		if [ "$POSTGRES_INITDB_XLOGDIR" ]; then
104
+			export POSTGRES_INITDB_ARGS="$POSTGRES_INITDB_ARGS --xlogdir $POSTGRES_INITDB_XLOGDIR"
105
+		fi
106
+		eval "initdb --username=postgres $POSTGRES_INITDB_ARGS"
107
+
108
+		# check password first so we can output the warning before postgres
109
+		# messes it up
110
+		file_env 'POSTGRES_PASSWORD'
111
+		if [ "$POSTGRES_PASSWORD" ]; then
112
+			pass="PASSWORD '$POSTGRES_PASSWORD'"
113
+			authMethod=md5
114
+		else
115
+			# The - option suppresses leading tabs but *not* spaces. :)
116
+			cat >&2 <<-'EOWARN'
117
+				****************************************************
118
+				WARNING: No password has been set for the database.
119
+				         This will allow anyone with access to the
120
+				         Postgres port to access your database. In
121
+				         Docker's default configuration, this is
122
+				         effectively any other container on the same
123
+				         system.
124
+
125
+				         Use "-e POSTGRES_PASSWORD=password" to set
126
+				         it in "docker run".
127
+				****************************************************
128
+			EOWARN
129
+
130
+			pass=
131
+			authMethod=trust
132
+		fi
133
+
134
+		{
135
+			echo
136
+			echo "host all all all $authMethod"
137
+		} >> "$PGDATA/pg_hba.conf"
138
+
139
+		# internal start of server in order to allow set-up using psql-client
140
+		# does not listen on external TCP/IP and waits until start finishes
141
+		PGUSER="${PGUSER:-postgres}" \
142
+		pg_ctl -D "$PGDATA" \
143
+			-o "-c listen_addresses='localhost'" \
144
+			-w start
145
+
146
+		file_env 'POSTGRES_USER' 'postgres'
147
+		file_env 'POSTGRES_DB' "$POSTGRES_USER"
148
+
149
+		echo
150
+
151
+                export PGUSER="postgres"
152
+                export PGDATABASE="postgres"
153
+                echo "Running core migrate"
154
+                migrate.py --folder /docker-entrypoint-initdb-core.d/ --init
155
+
156
+                export PGUSER="${POSTGRES_USER}"
157
+                export PGDATABASE="${POSTGRES_DB}"
158
+                echo "Running user migrate"
159
+                migrate.py --folder /docker-entrypoint-initdb.d/ --init
160
+
161
+		PGUSER="${PGUSER:-postgres}" \
162
+		pg_ctl -D "$PGDATA" -m fast -w stop
163
+
164
+		echo
165
+		echo 'PostgreSQL init process complete; ready for start up.'
166
+		echo
167
+	    fi
168
+        else
169
+            echo "Setting up slave..."
170
+
171
+            rm -rf "${PGDATA}"/*
172
+
173
+            PGPASSWORD="${POSTGRES_REP_PASSWORD}" pg_basebackup -h "${POSTGRES_REP_HOST}" -p "${POSTGRES_REP_PORT}" -D "${PGDATA}" -U "${POSTGRES_REP_USER}" -v
174
+
175
+            export PGUSER="postgres"
176
+            export PGDATABASE="postgres"
177
+            echo "Running core migrate"
178
+            migrate.py --folder /docker-entrypoint-initdb-core.d/ --init
179
+
180
+            echo
181
+            echo 'PostgreSQL init process complete; ready for start up.'
182
+            echo
183
+        fi
184
+fi
185
+
186
+exec "$@"

+ 140
- 0
migrate.py View File

@@ -0,0 +1,140 @@
1
+#! /usr/bin/env python3
2
+
3
+import argparse
4
+import sys
5
+import os
6
+import os.path
7
+import re
8
+import distutils.version
9
+import subprocess
10
+
11
+
12
+def file_runner_pgsql(folder, file):
13
+    subprocess.call(['psql', '-f', os.path.join(folder, file)])
14
+
15
+def file_runner_pgsql_gz(folder, file):
16
+    p1 = subprocess.Popen(['gunzip', '-c', os.path.join(folder, file)], stdout=subprocess.PIPE)
17
+    p2 = subprocess.Popen(["psql"], stdin=p1.stdout)
18
+    p1.stdout.close()
19
+    p2.communicate()
20
+
21
+def file_runner_exec(folder, file):
22
+    subprocess.call([os.path.join(folder, file)])
23
+
24
+
25
+def list_files(folder):
26
+    files = [f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))]
27
+    files.sort()
28
+    return files
29
+
30
+def sort_versions(versions):
31
+    versions.sort(key=distutils.version.StrictVersion)
32
+
33
+def list_versions(files):
34
+    versions = []
35
+    for file in files:
36
+        version = file.split('_')[0]
37
+        if re.match('^[0-9]+\.[0-9]+\.[0-9]+$', version):
38
+            if version not in versions:
39
+                versions.append(version)
40
+    sort_versions(versions)
41
+    return versions
42
+
43
+def run_file(folder, file, no_run, file_runners):
44
+    can_run = False
45
+    for file_runner_ext in file_runners:
46
+        if file.endswith(file_runner_ext):
47
+            print('Running file %s' % (file))
48
+            can_run = True
49
+            if not no_run:
50
+                try:
51
+                    sys.stdout.flush()
52
+                    file_runners[file_runner_ext](folder, file)
53
+                except Exception as e:
54
+                    print('Failed to run file: %s' % (e))
55
+    if not can_run:
56
+        print('Ignoring file %s' % (file))
57
+
58
+def run_migration(folder, files, version, no_run, file_runners):
59
+    for file in files:
60
+        if file.startswith('%s_' % (version)):
61
+            run_file(folder, file, no_run, file_runners)
62
+
63
+def run_migrations(folder, version_from, version_to, no_run, file_runners):
64
+    files = list_files(folder)
65
+    versions = list_versions(files)
66
+
67
+    if not versions:
68
+        print('No migration available. Exiting.')
69
+        return 0
70
+
71
+    if version_to is None:
72
+        version_to = versions[-1]
73
+        print('Defaulting VERSION_TO to %s' % (version_to))
74
+    if version_to not in versions:
75
+        print('Could not find VERSION_TO %s' % (version_to))
76
+        return 1
77
+    if version_from is not None and version_from not in versions:
78
+        print('Could not find VERSION_FROM %s' % (version_from))
79
+        return 1
80
+
81
+    if no_run:
82
+        print('Not running because --no-run was specified')
83
+
84
+    if version_from is None:
85
+        print('Initialising from %s (inclusive) to %s (inclusive)' % (versions[0], version_to))
86
+    else:
87
+        print('Migrating from %s (exclusive) to %s (inclusive)' % (version_from, version_to))
88
+
89
+    version_from_obj = distutils.version.StrictVersion('0.0.0' if version_from is None else version_from)
90
+    version_to_obj = distutils.version.StrictVersion(version_to)
91
+
92
+    last_version = version_from
93
+    for version in versions:
94
+        version_obj = distutils.version.StrictVersion(version)
95
+        if version_from_obj < version_obj and version_obj <= version_to_obj:
96
+            if last_version is None:
97
+                print('Initialising %s' % (version))
98
+            else:
99
+                print('Migrating from %s to %s' % (last_version, version))
100
+            run_migration(folder, files, version, no_run, file_runners)
101
+            last_version = version
102
+
103
+    return 0
104
+
105
+def main():
106
+    parser = argparse.ArgumentParser(description='Migrate container configuration/data')
107
+    parser.add_argument('--folder', dest='folder', default='/docker-entrypoint-initdb.d/', help='Folder to use for migration')
108
+    parser.add_argument('--init', dest='is_init', default=False, action='store_true', help='Run all migrations')
109
+    parser.add_argument('--migrate', dest='is_migrate', default=False, action='store_true', help='Run all migrations between VERSION_FROM (exclusive) and VERSION_TO (inclusive)')
110
+    parser.add_argument('--no-run', dest='no_run', default=False, action='store_true', help='Do not run migration, just print')
111
+    parser.add_argument('--version-from', dest='version_from', default=None, help='Current version, required if using --migrate, can not be used --init')
112
+    parser.add_argument('--version-to', dest='version_to', default=None, help='Final version, default to last available version is not given')
113
+
114
+    args = parser.parse_args()
115
+
116
+    file_runners = {
117
+            'sql': file_runner_pgsql,
118
+            'sql.gz': file_runner_pgsql_gz,
119
+            'sh': file_runner_exec
120
+            }
121
+
122
+    if args.is_init and args.is_migrate:
123
+        print('--init and --migrate can not be used together')
124
+        return 64
125
+    if args.is_init:
126
+        if args.version_from is not None:
127
+            print('--version-from can not be used with --init')
128
+            return 64
129
+        return run_migrations(args.folder, None, args.version_to, args.no_run, file_runners)
130
+    elif args.is_migrate:
131
+        if args.version_from is None:
132
+            print('--version-from is required. Use --init to run all migrations')
133
+            return 64
134
+        return run_migrations(args.folder, args.version_from, args.version_to, args.no_run, file_runners)
135
+    else:
136
+        print('Missing --init or --migrate')
137
+        return 64
138
+
139
+if __name__ == '__main__':
140
+    sys.exit(main())

+ 0
- 5
run.sh View File

@@ -1,5 +0,0 @@
1
-#! /usr/bin/env bash
2
-
3
-cron -f &
4
-
5
-/docker-entrypoint.sh $*

Loading…
Cancel
Save