pgbundle 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|