prodder 1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ require 'prodder/version'
2
+ require 'prodder/config'
3
+
4
+ module Prodder
5
+ end
@@ -0,0 +1,135 @@
1
+ require 'prodder'
2
+ require 'thor'
3
+ require 'yaml'
4
+
5
+ require 'pp' # TODO rm
6
+
7
+ class Prodder::CLI < Thor
8
+ include Thor::Actions
9
+
10
+ method_option :config, type: :string, aliases: '-c'
11
+ method_option :workspace, type: :string, aliases: '-w', default: File.join(Dir.pwd, 'prodder-workspace')
12
+
13
+ def initialize(*args)
14
+ super
15
+
16
+ # Help isn't printed when we don't provide --config, which is friggin absurd.
17
+ if options[:config].nil?
18
+ help
19
+ raise Thor::RequiredArgumentMissingError, "No value provided for required option '--config'"
20
+ end
21
+ end
22
+
23
+ desc "init [*PROJECTS]", "Initialize the named projects"
24
+ def init(*projects)
25
+ select_projects(projects).each { |project| project.init }
26
+
27
+ rescue Prodder::Git::GitError => ex
28
+ puts "Failed to run '#{ex.command}':"
29
+ puts ex.error
30
+ exit 1
31
+ end
32
+
33
+ desc "dump [*PROJECTS]", "Dump production data into the named projects"
34
+ def dump(*projects)
35
+ select_projects(projects).each do |project|
36
+ project.dump
37
+ if project.git.dirty?
38
+ puts "#{project.name}: updates introduced."
39
+ end
40
+ end
41
+
42
+ rescue Prodder::Project::SeedConfigFileMissing => ex
43
+ puts "No such file: #{ex.filename}"
44
+ exit 1
45
+
46
+ rescue Prodder::PG::PGDumpError => ex
47
+ puts ex.message
48
+ exit 1
49
+ end
50
+
51
+ desc "commit [*PROJECTS]", "Commit prodder data changes to the named projects"
52
+ def commit(*projects)
53
+ select_projects(projects).each do |project|
54
+ if project.git.dirty?
55
+ puts "#{project.name}: committing changes."
56
+ project.commit
57
+ else
58
+ puts "#{project.name}: no changes to commit."
59
+ end
60
+ end
61
+
62
+ rescue Prodder::Git::GitError => ex
63
+ puts "Failed to run '#{ex.command}':"
64
+ puts ex.error
65
+ exit 1
66
+ end
67
+
68
+ desc "push [*PROJECTS]", "Push new commits to the remote repositories of the named projects"
69
+ def push(*projects)
70
+ select_projects(projects).each do |project|
71
+ if project.nothing_to_push?
72
+ puts "#{project.name}: nothing to push."
73
+ else
74
+ puts "#{project.name}: pushing new commit."
75
+ project.push
76
+ end
77
+ end
78
+
79
+ rescue Prodder::Git::NotFastForward => ex
80
+ puts "Refusing to push to remote #{ex.remote}: origin/master is not a fast forward from master."
81
+ exit 1
82
+
83
+ rescue Prodder::Git::GitError => ex
84
+ puts "Failed to run '#{ex.command}':"
85
+ puts ex.error
86
+ exit 1
87
+ end
88
+
89
+ desc "ls", "List all known projects"
90
+ def ls(*projects)
91
+ config.projects.each { |project| puts project.name }
92
+ end
93
+
94
+ private
95
+
96
+ def select_projects(projects)
97
+ config.select_projects(projects) do |undefined|
98
+ puts "Project not defined: #{undefined.join(', ')}"
99
+ exit 1
100
+ end
101
+ end
102
+
103
+ def config
104
+ return @config if @config
105
+
106
+ contents = File.read options[:config]
107
+ projects = YAML.load contents
108
+
109
+ @config = Prodder::Config.new(projects).tap do |config|
110
+ config.workspace = options[:workspace]
111
+ config.lint!
112
+ end
113
+
114
+ rescue Errno::ENOENT
115
+ puts "Config file not found: #{options[:config]}"
116
+ exit 1
117
+ rescue Psych::SyntaxError
118
+ puts "Invalid YAML in config file #{options[:config]}. Current file contents:\n\n#{contents}"
119
+ exit 1
120
+
121
+ rescue Prodder::Config::PathError => ex
122
+ puts "`#{ex.message}` could not be found on your $PATH."
123
+ puts
124
+ puts "Current PATH:\n#{ENV['PATH']}"
125
+ exit 1
126
+
127
+ rescue Prodder::Config::LintError => ex
128
+ puts ex.errors.join("\n")
129
+ puts
130
+ puts "Example configuration:"
131
+ puts Prodder::Config.example_contents
132
+ exit 1
133
+ end
134
+
135
+ end
@@ -0,0 +1,95 @@
1
+ require 'prodder/project'
2
+ require 'yaml'
3
+
4
+ module Prodder
5
+ class Config
6
+ PathError = Class.new(StandardError)
7
+
8
+ LintError = Class.new(StandardError) do
9
+ attr_reader :errors
10
+
11
+ def initialize(errors)
12
+ @errors = errors
13
+ super errors.join "\n"
14
+ end
15
+ end
16
+
17
+ attr_accessor :workspace
18
+
19
+ def initialize(project_definitions)
20
+ @config = project_definitions
21
+ end
22
+
23
+ def assert_in_path(*cmds)
24
+ path = ENV['PATH'].split(File::PATH_SEPARATOR)
25
+ cmds.each do |cmd|
26
+ raise PathError.new(cmd) unless path.find { |dir| File.exist? File.join(dir, cmd) }
27
+ end
28
+ end
29
+
30
+ def lint
31
+ assert_in_path 'pg_dump'
32
+ assert_in_path 'git'
33
+
34
+ required = {
35
+ 'structure_file' => [],
36
+ 'seed_file' => [],
37
+ 'db' => %w[name host user tables],
38
+ 'git' => %w[origin author]
39
+ }
40
+
41
+ @config.each_with_object([]) do |(project, defn), errors|
42
+ required.each do |top, inner|
43
+ if !defn.key?(top)
44
+ errors << "Missing required configuration key: #{project}/#{top}"
45
+ else
46
+ errors.concat inner.reject { |key| defn[top].key?(key) }.map { |key|
47
+ "Missing required configuration key: #{project}/#{top}/#{key}"
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def lint!
55
+ lint.tap { |errors| raise LintError.new(errors) if errors.any? }
56
+ end
57
+
58
+ def projects
59
+ @projects ||= @config.map { |name, defn| Project.new(name, File.join(workspace, name), defn) }
60
+ end
61
+
62
+ def select_projects(names)
63
+ return projects if names.empty?
64
+
65
+ matches = projects.select { |project| names.include?(project.name) }
66
+
67
+ if matches.size != names.size
68
+ unmatched = names - matches.map(&:name)
69
+ yield unmatched if block_given?
70
+ end
71
+
72
+ matches
73
+ end
74
+
75
+ def self.example_contents
76
+ <<-EOF.gsub(/^ /, '')
77
+ blog:
78
+ structure_file: db/structure.sql
79
+ seed_file: db/seeds.sql
80
+ quality_check_file: db/quality_checks.sql
81
+ git:
82
+ origin: git@github.com:your/repo.git
83
+ author: prodder <prodder@example.com>
84
+ db:
85
+ name: database_name
86
+ host: database.server.example.com
87
+ user: username
88
+ password: password
89
+ tables:
90
+ - posts
91
+ - authors
92
+ EOF
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,97 @@
1
+ module Prodder
2
+ class Git
3
+ GitError = Class.new(StandardError) do
4
+ attr_reader :command, :error
5
+ def initialize(command, error); @command, @error = command, error; end
6
+ end
7
+
8
+ NotFoundError = Class.new(StandardError)
9
+ NotFastForward = Class.new(StandardError) do
10
+ attr_reader :remote
11
+ def initialize(remote); @remote = remote; end
12
+ end
13
+
14
+ def initialize(local, remote)
15
+ @local = local
16
+ @remote = remote
17
+ end
18
+
19
+ def clone_or_remote_update
20
+ if File.directory? File.join(@local, '.git')
21
+ remote_update
22
+ checkout 'master'
23
+ reset 'origin/master', true
24
+ else
25
+ clone
26
+ end
27
+ end
28
+
29
+ def dirty?
30
+ inside_repo { git('status', '--porcelain') != '' }
31
+ end
32
+
33
+ def tracked?(file)
34
+ inside_repo { git('ls-files', file) != '' }
35
+ end
36
+
37
+ def no_new_commits?
38
+ inside_repo do
39
+ git('show-ref', '--hash', 'origin/master') == git('show-ref', '--hash', 'refs/heads/master')
40
+ end
41
+ end
42
+
43
+ def fast_forward?
44
+ inside_repo do
45
+ git('merge-base', 'master', 'origin/master') == git('show-ref', '--hash', 'origin/master')
46
+ end
47
+ end
48
+
49
+ def clone
50
+ git 'clone', @remote, @local
51
+ end
52
+
53
+ def remote_update
54
+ inside_repo { git 'remote', 'update' }
55
+ end
56
+
57
+ def checkout(branch)
58
+ inside_repo { git 'checkout', branch }
59
+ end
60
+
61
+ def reset(sharef, hard = false)
62
+ inside_repo { git 'reset', hard ? '--hard' : '', sharef }
63
+ end
64
+
65
+ def add(file)
66
+ inside_repo { git 'add', file }
67
+ end
68
+
69
+ def commit(message, author)
70
+ inside_repo { git 'commit', "--author='#{author}'", "-m", message }
71
+ end
72
+
73
+ def push
74
+ inside_repo { git 'push', 'origin', 'master:master' }
75
+ end
76
+
77
+ private
78
+
79
+ def inside_repo(&block)
80
+ Dir.chdir @local, &block
81
+ end
82
+
83
+ def git(*cmd, &block)
84
+ cmd = ['git', *cmd]
85
+
86
+ Open3.popen3(*cmd) do |stdin, out, err, thr|
87
+ out = out.read
88
+ err = err.read
89
+ raise GitError.new(cmd.join(' '), err) if !thr.value.success?
90
+ block.call(out, err, thr) if block
91
+ out
92
+ end
93
+ rescue Errno::ENOENT
94
+ raise NotFoundError
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,486 @@
1
+ require 'open3'
2
+ require 'pg'
3
+
4
+ module Prodder
5
+ class PG
6
+ PGDumpError = Class.new(StandardError)
7
+
8
+ attr_reader :credentials
9
+
10
+ def initialize(credentials = {})
11
+ @credentials = credentials
12
+ end
13
+
14
+ def create_role(role, opts = [])
15
+ arguments = [
16
+ 'createuser',
17
+ '--no-password',
18
+ '--no-superuser',
19
+ '--no-createrole',
20
+ '--no-createdb'
21
+ ]
22
+
23
+ arguments.push *opts, role
24
+ run arguments
25
+ end
26
+
27
+ def drop_role(role)
28
+ run ['dropuser', role]
29
+ end
30
+
31
+ def create_db(db_name, sql = nil)
32
+ run ['createdb', db_name]
33
+ psql db_name, sql if sql
34
+ end
35
+
36
+ def drop_db(db_name)
37
+ run ['dropdb', db_name]
38
+ end
39
+
40
+ def psql(db_name, sql)
41
+ run ['psql', db_name], sql
42
+ end
43
+
44
+ def dump_settings(db_name, filename)
45
+ sql = <<-SQL
46
+ select unnest(setconfig)
47
+ from pg_catalog.pg_db_role_setting
48
+ join pg_database on pg_database.oid = setdatabase
49
+ -- 0 = default, for all users
50
+ where setrole = 0
51
+ and datname = '#{db_name}'
52
+ SQL
53
+
54
+ arguments = [
55
+ '-t',
56
+ '-c', sql
57
+ ]
58
+
59
+ run ['psql', *arguments.push(db_name)] do |out, err, success|
60
+ raise PGDumpError.new(err) if !success
61
+ File.open(filename, 'w') do |f|
62
+ out.each_line do |setting|
63
+ f.write "ALTER DATABASE #{db_name} SET #{setting}" unless setting.gsub(/\s+/, '').empty?
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def dump_structure(db_name, filename, options = {})
70
+ arguments = [
71
+ '--schema-only',
72
+ '--no-privileges',
73
+ '--no-owner',
74
+ '--host', credentials['host'],
75
+ '--username', credentials['user']
76
+ ]
77
+
78
+ if options[:exclude_schemas].respond_to? :map
79
+ arguments.concat options[:exclude_schemas].map { |schema| ['--exclude-schema', schema] }.flatten
80
+ end
81
+
82
+ if options[:exclude_tables].respond_to? :map
83
+ arguments.concat options[:exclude_tables].map { |table| ['--exclude-table', table] }.flatten
84
+ end
85
+
86
+ pg_dump filename, arguments.push(db_name)
87
+ end
88
+
89
+ def dump_tables(db_name, tables, filename)
90
+ pg_dump filename, [
91
+ '--data-only',
92
+ '--no-privileges',
93
+ '--no-owner',
94
+ '--disable-triggers',
95
+ '--host', credentials['host'],
96
+ '--username', credentials['user'],
97
+ *tables.map { |table| ['--table', table] }.flatten,
98
+ db_name
99
+ ]
100
+ end
101
+
102
+ def dump_permissions(db_name, filename, options = {})
103
+ perm_out_sql = ""
104
+ user_list = []
105
+
106
+ perm_out_sql << dump_db_access_control(db_name, user_list, options)
107
+ perm_out_sql.prepend pg_dumpall db_name, user_list, options
108
+
109
+ perm_out_sql.prepend(alter_role_function)
110
+ perm_out_sql.prepend(create_role_function)
111
+ perm_out_sql.prepend(granted_by_function)
112
+ perm_out_sql.prepend("SET client_min_messages TO WARNING;\n")
113
+ perm_out_sql << drop_role_create_function
114
+ perm_out_sql << drop_role_alter_function
115
+ perm_out_sql << drop_granted_by_function
116
+
117
+ File.open(filename, 'w') { |f| f.write perm_out_sql }
118
+ end
119
+
120
+ #From pg_dump
121
+ ACL_GRANT = /^GRANT /
122
+ ACL_REVOKE = /^REVOKE /
123
+ DEFAULT_PRIVILEGES = /^ALTER DEFAULT PRIVILEGES /
124
+ SET_OBJECT_OWNERSHIP = /.* OWNER TO /
125
+ SEARCH_PATH = /SET search_path = .*/
126
+
127
+ def dump_db_access_control(db_name, user_list, options)
128
+ perm_out_sql = ""
129
+ arguments = [
130
+ '--schema-only',
131
+ '--host', credentials['host'],
132
+ '--username', credentials['user']
133
+ ]
134
+
135
+ if options[:exclude_schemas].respond_to? :map
136
+ arguments.concat options[:exclude_schemas].map { |schema| ['--exclude-schema', schema] }.flatten
137
+ end
138
+
139
+ if options[:exclude_tables].respond_to? :map
140
+ arguments.concat options[:exclude_tables].map { |table| ['--exclude-table', table] }.flatten
141
+ end
142
+
143
+ arguments.push db_name
144
+
145
+ white_list = options[:included_users] || []
146
+ irrelevant_login_roles = irrelevant_login_roles(db_name, white_list).map { |user| user['rolname'] }
147
+
148
+ run ['pg_dump', *arguments] do |out, err, success|
149
+ out.each_line do |line|
150
+ if line.match(ACL_GRANT) ||
151
+ line.match(ACL_REVOKE) ||
152
+ line.match(DEFAULT_PRIVILEGES) ||
153
+ line.match(SET_OBJECT_OWNERSHIP)||
154
+ line.match(SEARCH_PATH)
155
+
156
+ unless irrelevant_login_roles.include?(line.match(/ (\S*);$/)[1])
157
+ perm_out_sql << line
158
+ user_list << (line.match(/ (\S*);$/)[1]).gsub(/"/, '')
159
+ end
160
+ end
161
+ end
162
+ raise PGDumpError.new(err) if !success
163
+ end
164
+
165
+ user_list.uniq!
166
+ perm_out_sql
167
+ end
168
+
169
+ private
170
+
171
+ def pg_conn(db_name)
172
+ conn = ::PG.connect(
173
+ dbname: db_name,
174
+ host: credentials['host'],
175
+ user: credentials['user'],
176
+ password: credentials['password']
177
+ )
178
+
179
+ res = yield(conn)
180
+ conn.close
181
+ res
182
+ end
183
+
184
+ def irrelevant_login_roles(db_name, white_list)
185
+ white_list ||= []
186
+ login_role_list = pg_conn(db_name) do |conn|
187
+ conn.exec('SELECT oid, rolname FROM pg_roles WHERE rolcanlogin AND NOT rolsuper').map do |role|
188
+ {'oid' => role['oid'], 'rolname' => role['rolname'] }
189
+ end
190
+ end
191
+
192
+ login_role_list.reject! { |user| white_list.include?(user['rolname']) }
193
+ login_role_list
194
+ end
195
+
196
+ def run(cmd, stdin_data = nil, &block)
197
+ # TODO use a tmp .pgpass file instead of $PGPASSWORD
198
+ env = { 'PGPASSWORD' => credentials['password'] }
199
+ Open3.popen3(env, *cmd) do |stdin, out, err, thr|
200
+ if stdin_data
201
+ stdin.write stdin_data
202
+ stdin.close
203
+ end
204
+
205
+ out, err = out.read, err.read
206
+ puts err if err
207
+ block.call(out, err, thr.value.success?) if block
208
+ out
209
+ end
210
+ end
211
+
212
+ def pg_dump(filename, cmd)
213
+ run ['pg_dump', *cmd] do |out, err, success|
214
+ raise PGDumpError.new(err) if !success
215
+ File.open(filename, 'w') { |f| f.write out }
216
+ end
217
+ end
218
+
219
+ def pg_dumpall(db_name, user_list, options)
220
+ white_list = options[:included_users] || []
221
+ irrelevant_login_roles = irrelevant_login_roles(db_name, white_list).map { |user| user['oid'] }
222
+
223
+ roles_and_memberships = pg_conn(db_name) { |conn| conn.exec smart_pgdumpall(user_list, irrelevant_login_roles) }
224
+
225
+ rolcreate_sql, rolalter_sql, rolgrant_sql = "", "", ""
226
+ created_roles = Set.new
227
+
228
+ roles_and_memberships.each do |role|
229
+ unless created_roles.member? role['rolname']
230
+ created_roles << role['rolname']
231
+ tmp_sql = ""
232
+ rolcreate_sql << "SELECT * FROM create_role_if_not_exists('#{role['rolname']}');\n"
233
+ tmp_sql << "ALTER ROLE \"#{role['rolname']}\" WITH"
234
+
235
+ [
236
+ ['rolsuper', 'SUPERUSER'],
237
+ ['rolinherit', 'INHERIT'],
238
+ ['rolcreaterole', 'CREATEROLE'],
239
+ ['rolcreatedb', 'CREATEDB'],
240
+ ['rolcanlogin', 'LOGIN'],
241
+ ['rolreplication', 'REPLICATION']
242
+ ].each do |key, modifier|
243
+ tmp_sql << if role[key].eql? 't'
244
+ " #{modifier}"
245
+ else
246
+ modifier.prepend ' NO'
247
+ end
248
+ end
249
+
250
+ tmp_sql << " CONNECTION LIMIT #{role['rolconnlimit']}" unless role['rolconnlimit'].eql?("-1")
251
+ tmp_sql << " VALID UNTIL '#{role['rolvaliduntil']}'" unless role['rolvaliduntil'].nil?
252
+ tmp_sql << ";\n"
253
+ tmp_sql << "COMMENT ON ROLE \"#{role['rolname']}\" IS '#{role['rolcomment']}';\n" unless role['rolcomment'].nil?
254
+ rolalter_sql << "SELECT * FROM alter_role('#{role['rolname']}', $$#{tmp_sql}$$);\n"
255
+ end
256
+
257
+ unless role['member'].nil?
258
+ tmp_sql = ""
259
+ rolgrant_sql << "GRANT \"#{role['roleid']}\" TO \"#{role['member']}\""
260
+ rolgrant_sql << " WITH ADMIN OPTION" if role['admin_option'].eql?('t')
261
+ rolgrant_sql << ";\n"
262
+ tmp_sql << "GRANT \"#{role['roleid']}\" TO \"#{role['member']}\""
263
+ tmp_sql << " WITH ADMIN OPTION" if role['admin_option'].eql?('t')
264
+ tmp_sql << " GRANTED BY #{role['grantor']}" if role['grantor'].eql?('t')
265
+ tmp_sql << ";"
266
+ rolgrant_sql << "SELECT * FROM granted_by('#{role['grantor']}', $$#{tmp_sql}$$);\n"
267
+ end
268
+ end
269
+
270
+ rolcreate_sql << rolalter_sql << rolgrant_sql
271
+ end
272
+
273
+ def smart_pgdumpall(user_list, irrelevant_login_roles)
274
+ irrelevant_login_roles << -1 if irrelevant_login_roles.respond_to?(:empty?) && irrelevant_login_roles.empty?
275
+ replace_bind_variables(<<-SQL, [user_list, irrelevant_login_roles])
276
+ WITH RECURSIVE memberships(roleid, member, admin_option, grantor) AS (
277
+ SELECT ur.oid AS roleid,
278
+ NULL::oid AS member,
279
+ NULL::boolean AS admin_option,
280
+ NULL::oid AS grantor
281
+ FROM pg_roles ur
282
+ WHERE ur.rolname IN (?)
283
+ UNION
284
+ SELECT COALESCE(a.roleid, r.oid) AS roleid,
285
+ a.member AS member,
286
+ a.admin_option AS admin_option,
287
+ a.grantor AS grantor
288
+ FROM pg_auth_members a
289
+ FULL OUTER JOIN pg_roles r ON FALSE
290
+ JOIN memberships
291
+ ON (memberships.roleid = a.member)
292
+ OR (memberships.roleid = r.oid OR memberships.member = r.oid)
293
+ OR (memberships.roleid = a.roleid AND COALESCE(memberships.member, 0::oid) <> a.member AND a.member NOT IN(?))
294
+ )
295
+ SELECT DISTINCT ON(ur.rolname, um.rolname)
296
+ ur.rolname AS roleid,
297
+ um.rolname AS member,
298
+ memberships.admin_option,
299
+ ug.rolname AS grantor,
300
+ ur.rolname, ur.rolsuper, ur.rolinherit,
301
+ ur.rolcreaterole, ur.rolcreatedb,
302
+ ur.rolcanlogin, ur.rolconnlimit,
303
+ ur.rolvaliduntil, ur.rolreplication,
304
+ pg_catalog.shobj_description(memberships.roleid, 'pg_authid') as rolcomment
305
+ FROM memberships
306
+ LEFT JOIN pg_roles ur on ur.oid = memberships.roleid
307
+ LEFT JOIN pg_roles um on um.oid = memberships.member
308
+ LEFT JOIN pg_roles ug on ug.oid = memberships.grantor
309
+ ORDER BY 1,2 NULLS LAST;
310
+ SQL
311
+ end
312
+
313
+ def replace_bind_variable(value)
314
+ if value.respond_to?(:map)
315
+ if value.respond_to?(:empty?) && value.empty?
316
+ quote(nil)
317
+ else
318
+ value.map { |v| quote(v) }.join(',')
319
+ end
320
+ else
321
+ quote(value)
322
+ end
323
+ end
324
+
325
+ def quote_string(s)
326
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''")
327
+ end
328
+
329
+ def quote(value)
330
+ "'#{quote_string(value.to_s)}'"
331
+ end
332
+
333
+ def replace_bind_variables(statement, values)
334
+ values.each do |value|
335
+ statement.sub!(/\?/) do
336
+ replace_bind_variable(value)
337
+ end
338
+ end
339
+ statement
340
+ end
341
+
342
+ def alter_role_function
343
+ <<-SQL
344
+ CREATE OR REPLACE FUNCTION public.alter_role(rolename VARCHAR, sql TEXT)
345
+ RETURNS TEXT
346
+ AS
347
+ $alter_role$
348
+ DECLARE
349
+ r RECORD;
350
+ compensating_sql TEXT := '**!!**ALTER ROLE ';
351
+ BEGIN
352
+ EXECUTE 'SELECT rolsuper, rolinherit,
353
+ rolcreaterole, rolcreatedb,
354
+ rolcanlogin, rolconnlimit,
355
+ rolvaliduntil, rolreplication,
356
+ pg_catalog.shobj_description(oid, $1) as rolcomment
357
+ FROM pg_roles
358
+ WHERE rolname = $2'
359
+ INTO r
360
+ USING 'pg_authid', rolename;
361
+ compensating_sql := compensating_sql || '"' || rolename || '" WITH';
362
+ IF r.rolsuper THEN
363
+ compensating_sql := compensating_sql || ' SUPERUSER';
364
+ ELSE
365
+ compensating_sql := compensating_sql || ' NOSUPERUSER';
366
+ END IF;
367
+ IF r.rolinherit THEN
368
+ compensating_sql := compensating_sql || ' INHERIT';
369
+ ELSE
370
+ compensating_sql := compensating_sql || ' NOINHERIT';
371
+ END IF;
372
+ IF r.rolcreaterole THEN
373
+ compensating_sql := compensating_sql || ' CREATEROLE';
374
+ ELSE
375
+ compensating_sql := compensating_sql || ' NOCREATEROLE';
376
+ END IF;
377
+ IF r.rolcreatedb THEN
378
+ compensating_sql := compensating_sql || ' CREATEDB';
379
+ ELSE
380
+ compensating_sql := compensating_sql || ' NOCREATEDB';
381
+ END IF;
382
+ IF r.rolcanlogin THEN
383
+ compensating_sql := compensating_sql || ' LOGIN';
384
+ ELSE
385
+ compensating_sql := compensating_sql || ' NOLOGIN';
386
+ END IF;
387
+ IF r.rolreplication THEN
388
+ compensating_sql := compensating_sql || ' REPLICATION';
389
+ ELSE
390
+ compensating_sql := compensating_sql || ' NOREPLICATION';
391
+ END IF;
392
+ IF r.rolconnlimit <> -1 THEN
393
+ compensating_sql := compensating_sql || ' CONNECTION LIMIT ' || r.rolconnlimit;
394
+ END IF;
395
+ IF r.rolvaliduntil IS NOT NULL THEN
396
+ compensating_sql := compensating_sql || ' VALID UNTIL ' || r.rolvaliduntil;
397
+ END IF;
398
+ compensating_sql := compensating_sql || ';\n';
399
+ IF r.rolcomment IS NOT NULL THEN
400
+ compensating_sql := compensating_sql || 'COMMENT ON ROLE "' || rolename || '" IS ' || r.rolcomment || ';\n';
401
+ END IF;
402
+ compensating_sql := compensating_sql || '**!!**';
403
+ EXECUTE sql;
404
+ RETURN compensating_sql;
405
+ END;
406
+ $alter_role$
407
+ LANGUAGE PLPGSQL;
408
+ SQL
409
+ end
410
+
411
+ def drop_role_alter_function
412
+ <<-SQL
413
+ \n
414
+ DROP FUNCTION public.alter_role(VARCHAR, TEXT);
415
+ \n
416
+ SQL
417
+ end
418
+
419
+ def granted_by_function
420
+ <<-SQL
421
+ \n
422
+ CREATE OR REPLACE FUNCTION public.granted_by(rolename VARCHAR, sql TEXT)
423
+ RETURNS BOOLEAN
424
+ AS
425
+ $role_exists$
426
+ DECLARE
427
+ BEGIN
428
+ IF EXISTS (
429
+ SELECT *
430
+ FROM pg_catalog.pg_roles
431
+ WHERE rolname = rolename) THEN
432
+ EXECUTE sql;
433
+ RETURN TRUE;
434
+ ELSE
435
+ RAISE NOTICE 'Rolename % does not exist, cannot set granted by', rolename;
436
+ RETURN FALSE;
437
+ END IF;
438
+ END;
439
+ $role_exists$
440
+ LANGUAGE PLPGSQL;
441
+ \n
442
+ SQL
443
+ end
444
+
445
+ def drop_granted_by_function
446
+ <<-SQL
447
+ \n
448
+ DROP FUNCTION public.granted_by(VARCHAR, TEXT);
449
+ \n
450
+ SQL
451
+ end
452
+
453
+ def create_role_function
454
+ <<-SQL
455
+ \n
456
+ CREATE OR REPLACE FUNCTION public.create_role_if_not_exists(rolename VARCHAR)
457
+ RETURNS TEXT
458
+ AS
459
+ $create_role_if_not_exists$
460
+ DECLARE
461
+ BEGIN
462
+ IF NOT EXISTS (
463
+ SELECT *
464
+ FROM pg_catalog.pg_roles
465
+ WHERE rolname = rolename) THEN
466
+ EXECUTE 'CREATE ROLE ' || quote_ident(rolename) || ' ;';
467
+ RETURN '**!!**DROP ROLE ''' || rolename || ''';**!!**';
468
+ ELSE
469
+ RETURN FALSE;
470
+ END IF;
471
+ END;
472
+ $create_role_if_not_exists$
473
+ LANGUAGE PLPGSQL;
474
+ \n
475
+ SQL
476
+ end
477
+
478
+ def drop_role_create_function
479
+ <<-SQL
480
+ \n
481
+ DROP FUNCTION public.create_role_if_not_exists(VARCHAR);
482
+ \n
483
+ SQL
484
+ end
485
+ end
486
+ end