pgbundle 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +45 -0
- data/Rakefile +7 -0
- data/bin/pgbundle +61 -0
- data/lib/pgbundle.rb +59 -0
- data/lib/pgbundle/base_source.rb +26 -0
- data/lib/pgbundle/database.rb +133 -0
- data/lib/pgbundle/definition.rb +70 -0
- data/lib/pgbundle/dsl.rb +44 -0
- data/lib/pgbundle/extension.rb +270 -0
- data/lib/pgbundle/github_source.rb +32 -0
- data/lib/pgbundle/path_source.rb +15 -0
- data/lib/pgbundle/version.rb +3 -0
- data/pgbundle.gemspec +30 -0
- data/spec/Pgfile +6 -0
- data/spec/definition_spec.rb +19 -0
- data/spec/dsl_spec.rb +8 -0
- data/spec/extension_spec.rb +76 -0
- data/spec/sample_extensions/bar/Makefile +5 -0
- data/spec/sample_extensions/bar/bar--0.0.2.sql +2 -0
- data/spec/sample_extensions/bar/bar.control +5 -0
- data/spec/sample_extensions/baz/Makefile +5 -0
- data/spec/sample_extensions/baz/baz--0.0.2.sql +2 -0
- data/spec/sample_extensions/baz/baz.control +5 -0
- data/spec/sample_extensions/foo/Makefile +5 -0
- data/spec/sample_extensions/foo/foo--0.0.1--0.0.2.sql +2 -0
- data/spec/sample_extensions/foo/foo--0.0.1.sql +2 -0
- data/spec/sample_extensions/foo/foo--0.0.2--0.0.1.sql +2 -0
- data/spec/sample_extensions/foo/foo--0.0.2.sql +2 -0
- data/spec/sample_extensions/foo/foo.control +4 -0
- data/spec/source_spec.rb +10 -0
- data/spec/spec_helper.rb +47 -0
- metadata +210 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/bin/pgbundle
ADDED
@@ -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)
|
data/lib/pgbundle.rb
ADDED
@@ -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
|