pgbundle 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7b9cc70db81cebbd38a1e0e58ed488170c31b63a
4
+ data.tar.gz: d53ffc2ec4eeb0ee68f4b2152bd454930d925171
5
+ SHA512:
6
+ metadata.gz: fd4ad7ddb2a3e04d5ac26e51fb56892a52682f5b88647799a652bca003089f7a6f3f3d15ef41f3c422d09983e84e7fce562caedec603a98fd7136988194fbb88
7
+ data.tar.gz: 1876743b60fae52ecc8a59c928703b4f9d42a7b00da3ae746eec089a7ef47e4cf3deb5774db3045d05338822c9e57187c243a4c1b94d0568968769891c8421d1
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - rbx-2
5
+ - 2.0.0
6
+ - 2.1.1
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: rbx-2
10
+ addons:
11
+ postgresql: "9.3"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pgbundle.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Manuel Kniep
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,45 @@
1
+ # pgbundle
2
+
3
+ bundling postgres extension
4
+
5
+ ## install
6
+
7
+ gem install pgbundle
8
+
9
+ ## usage
10
+
11
+ define your dependent postgres extensions in a Pgfile like this:
12
+
13
+ ```
14
+ #Pgfile
15
+
16
+ database 'my_database', host: 'my.db.server', use_sudo: true, system_user: 'postgres'
17
+
18
+ pgx 'hstore'
19
+ pgx 'my_extension', '1.0.2', github: me/my_extension
20
+ pgx 'my_other_extionsion', :git => 'https://github.com/me/my_other_extionsion.git'
21
+ pgx 'my_ltree_dependend_extension', github: me/my_ltree_dependend_extension, require: 'ltree'
22
+ ```
23
+
24
+ ### install your extension
25
+
26
+ pgbundle install
27
+
28
+ installs the extensions and dependencies on your database server
29
+
30
+ ### check your dependencies
31
+
32
+ pgbundle check
33
+
34
+ checks whether all dependencies are available for creation on the database server
35
+
36
+ ## getting started
37
+
38
+ if your already have some database on your current project you can get a starting point with
39
+
40
+ pgbundle init
41
+
42
+ lets say your database named 'my_project' runs on localhost with user postges
43
+
44
+ pgbundle init my_project -u postgres -h localhost
45
+
@@ -0,0 +1,7 @@
1
+ require 'rake'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ require 'thor'
3
+ require 'thor/group'
4
+ require 'pgbundle'
5
+ require 'pry'
6
+
7
+ module PgBundle
8
+ class Cli < Thor
9
+ desc 'install', 'installs extensions'
10
+ def install(pgfile = 'Pgfile')
11
+ definition(pgfile).available_extensions.each do |dep|
12
+ say_status('exists', dep.name)
13
+ end
14
+
15
+ installed = definition(pgfile).install
16
+
17
+ installed.each do |d|
18
+ say_status('install', d.name, :yellow)
19
+ end
20
+ rescue InstallError, ExtensionCreateError, CircularDependencyError => e
21
+ say_status('error', e.message, :red)
22
+ exit 1
23
+ end
24
+
25
+ desc 'check', 'checks availability of required extensions'
26
+ def check(pgfile = 'Pgfile')
27
+ missing = false
28
+ definition(pgfile).check.each do |d|
29
+ if d[:created]
30
+ say_status('created', d[:name])
31
+ else
32
+ unless d[:installed]
33
+ say_status('missing', d[:name], :red)
34
+ missing = true
35
+ end
36
+ say_status('installed', d[:name], :yellow) if d[:installed]
37
+ end
38
+ end
39
+ exit 1 if missing
40
+ end
41
+
42
+ desc 'init', 'write an initial pgfile to stdout'
43
+ method_options %w( user -u ) => :string
44
+ method_options %w( host -h ) => :string
45
+ def init(db_name)
46
+ definition = PgBundle::Definition.new
47
+ definition.database = PgBundle::Database.new(db_name, options)
48
+ say("found the following definition for your current database:\n\n")
49
+ say definition.init.join("\n")
50
+ end
51
+
52
+ no_commands do
53
+ def definition(pgfile)
54
+ definition = Dsl.new.eval_pgfile(pgfile)
55
+ definition.link_dependencies
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ PgBundle::Cli.start(ARGV)
@@ -0,0 +1,59 @@
1
+ require 'pgbundle/version'
2
+
3
+ module PgBundle
4
+ autoload :Dsl, 'pgbundle/dsl'
5
+ autoload :Definition, 'pgbundle/definition'
6
+ autoload :Database, 'pgbundle/database'
7
+ autoload :Extension, 'pgbundle/extension'
8
+ autoload :BaseSource, 'pgbundle/base_source'
9
+ autoload :PathSource, 'pgbundle/path_source'
10
+ autoload :GithubSource, 'pgbundle/github_source'
11
+
12
+ class PgfileError < StandardError
13
+ end
14
+
15
+ class InstallError < StandardError; end
16
+ class ExtensionCreateError < StandardError; end
17
+ class CircularDependencyError < StandardError
18
+ def initialize(base, dep)
19
+ super "Circular Dependency between #{base} and #{dep} detected"
20
+ end
21
+ end
22
+
23
+ class TransactionRollback < StandardError; end
24
+
25
+ class ExtensionNotFound < ExtensionCreateError
26
+ def initialize(name, version = nil)
27
+ if version
28
+ super "specified Version #{version} for Extension #{name} not available"
29
+ else
30
+ super "Extension #{name} not available"
31
+ end
32
+ end
33
+ end
34
+
35
+ class SourceNotFound < InstallError
36
+ def initialize(name)
37
+ super "Source for Extension #{name} not found"
38
+ end
39
+ end
40
+
41
+ class DependencyNotFound < ExtensionCreateError
42
+ def initialize(base_name, dependen_msg)
43
+ super "Can't install Dependency for Extension #{base_name}: #{dependen_msg}"
44
+ end
45
+ end
46
+
47
+ class MissingDependency < ExtensionCreateError
48
+ def initialize(base_name, dependen_msg)
49
+ required = dependen_msg[/required extension \"(.*?)\" is not installed/, 1]
50
+ super "Dependency #{required} for Extension #{base_name} is not defined"
51
+ end
52
+ end
53
+
54
+ class GitCommandError < InstallError
55
+ def initialize
56
+ super 'Failed to load git repository'
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ module PgBundle
2
+ # The BaseSource class defines an Extension source like PathSource or GithubSource
3
+ # it defines how to get the code and run make install on a given host (e.g. database server)
4
+ class BaseSource
5
+ attr_accessor :path
6
+ def initialize(path)
7
+ @path = path
8
+ end
9
+
10
+ def load(host, user, dest)
11
+ fail NotImplementedError
12
+ end
13
+
14
+ private
15
+
16
+ def copy_local(source, dest)
17
+ FileUtils.cp_r source, dest
18
+ end
19
+
20
+ def copy_to_remote(host, user, source, dest)
21
+ Net::SCP.start(host, user) do |scp|
22
+ scp.upload(source, dest, recursive: true)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,133 @@
1
+ require 'pg'
2
+ require 'pry'
3
+ module PgBundle
4
+ # The Database class defines on which database the extensions should be installed
5
+ # Note to install an extension the code must be compiled on the database server
6
+ # on a typical environment ssh access is needed if the database host differs from
7
+ # the Pgfile host
8
+ class Database
9
+ attr_accessor :name, :user, :host, :system_user, :use_sudo
10
+ def initialize(name, opts = {})
11
+ @name = name
12
+ @user = opts[:user] || 'postgres'
13
+ @host = opts[:host] || 'localhost'
14
+ @use_sudo = opts[:use_sudo] || false
15
+ @system_user = opts[:system_user] || 'postgres'
16
+ end
17
+
18
+ def connection
19
+ @connection ||= begin
20
+ PG.connect(dbname: name, user: user, host: host)
21
+ end
22
+ end
23
+
24
+ # executes the given sql on the database connections
25
+ # redirects all noise to /dev/null
26
+ def execute(sql)
27
+ silence do
28
+ connection.exec sql
29
+ end
30
+ end
31
+
32
+ def transaction(&block)
33
+ silence do
34
+ connection.transaction(&block)
35
+ end
36
+ end
37
+
38
+ def transaction_rollback(&block)
39
+ silence do
40
+ connection.transaction do |con|
41
+ yield con
42
+ fail TransactionRollback
43
+ end
44
+ end
45
+
46
+ rescue TransactionRollback
47
+ end
48
+
49
+ # loads the source, runs make install and removes the source afterwards
50
+ def make_install(source, ext_name)
51
+ remove_source(ext_name)
52
+ source.load(host, system_user, load_destination(ext_name))
53
+ run(make_install_cmd(ext_name))
54
+ remove_source(ext_name)
55
+ end
56
+
57
+ # loads the source and runs make uninstall
58
+ def make_uninstall(source, ext_name)
59
+ remove_source(ext_name)
60
+ source.load(host, system_user, load_destination(ext_name))
61
+ run(make_uninstall_cmd(ext_name))
62
+ remove_source(ext_name)
63
+ end
64
+
65
+ def drop_extension(name)
66
+ execute "DROP EXTENSION IF EXISTS #{name}"
67
+ end
68
+
69
+ def load_destination(ext_name)
70
+ "/tmp/#{ext_name}"
71
+ end
72
+
73
+ # returns currently installed extensions
74
+ def current_definition
75
+ result = execute('SELECT name, version, requires FROM pg_available_extension_versions WHERE installed').to_a
76
+ end
77
+
78
+ private
79
+
80
+ def sudo
81
+ use_sudo ? 'sudo' : ''
82
+ end
83
+
84
+ def remove_source(name)
85
+ run("rm -rf #{load_destination(name)}")
86
+ end
87
+
88
+ def make_install_cmd(name)
89
+ "cd #{load_destination(name)} && #{sudo} make clean && make install"
90
+ end
91
+
92
+ def make_uninstall_cmd(name)
93
+ "cd #{load_destination(name)} && #{sudo} make uninstall"
94
+ end
95
+
96
+ def run(cmd)
97
+ if host == 'localhost'
98
+ local cmd
99
+ else
100
+ remote cmd
101
+ end
102
+ end
103
+
104
+ def local(cmd)
105
+ %x(#{cmd})
106
+ end
107
+
108
+ def remote(cmd)
109
+ Net::SSH.start(host, system_user) do |ssh|
110
+ ssh.exec cmd
111
+ end
112
+ end
113
+
114
+
115
+ def silence
116
+ begin
117
+ orig_stderr = $stderr.clone
118
+ orig_stdout = $stdout.clone
119
+ $stderr.reopen File.new('/dev/null', 'w')
120
+ $stdout.reopen File.new('/dev/null', 'w')
121
+ retval = yield
122
+ rescue Exception => e
123
+ $stdout.reopen orig_stdout
124
+ $stderr.reopen orig_stderr
125
+ raise e
126
+ ensure
127
+ $stdout.reopen orig_stdout
128
+ $stderr.reopen orig_stderr
129
+ end
130
+ retval
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,70 @@
1
+ require 'pg'
2
+ module PgBundle
3
+ # The Definition class collects all objects defined in a PgFile
4
+ class Definition
5
+ attr_accessor :database, :extensions, :errors
6
+ def initialize
7
+ @extensions = {}
8
+ @errors = []
9
+ end
10
+
11
+ # returns an Array of missing Extensions
12
+ def missing_extensions
13
+ link_dependencies
14
+ extensions.select { |_, dep| !dep.available?(database) }.values
15
+ end
16
+
17
+ # returns an Array of already available Extensions
18
+ def available_extensions
19
+ link_dependencies
20
+ extensions.select { |_, dep| dep.available?(database) }.values
21
+ end
22
+
23
+ # installs missing extensions returns all successfully installed Extensions
24
+ def install
25
+ installed = missing_extensions.map do |dep|
26
+ dep.install(database)
27
+ dep
28
+ end
29
+
30
+ installed.select { |dep| dep.available?(database) }
31
+ end
32
+
33
+ def init
34
+ ["database '#{database.name}', host: '#{database.host}', user: #{database.user}, system_user: #{database.system_user}, use_sudo: #{database.use_sudo}"] +
35
+ database.current_definition.map do |r|
36
+ name, version = r['name'], r['version']
37
+ requires = r['requires'] ? ", requires: " + r['requires'].gsub(/[{},]/,{'{' => '%w(', '}' =>')', ','=> ' '}) : ''
38
+ "pgx '#{name}', '#{version}'#{requires}"
39
+ end
40
+ end
41
+
42
+ # returns an array hashes with dependency information
43
+ # [{name: 'foo', installed: true, created: false }]
44
+ def check
45
+ link_dependencies
46
+ extensions.map do |_,ext|
47
+ {
48
+ name: ext.name,
49
+ installed: ext.installed?(database),
50
+ created: ext.created?(database)
51
+ }
52
+ end
53
+ end
54
+
55
+ # links extension dependencies to each other
56
+ def link_dependencies
57
+ extensions.each do |_, ex|
58
+ undefined_dependencies = ex.dependencies.select { |k, v| v.source.nil? }.keys
59
+ undefined_dependencies.each do |name|
60
+ if extensions[name]
61
+ ex.dependencies[name] = extensions[name]
62
+ else
63
+ ex.dependencies[name] = PgBundle::Extension.new(name)
64
+ end
65
+ end
66
+ end
67
+ self
68
+ end
69
+ end
70
+ end