pgai 0.1.4 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -2
- data/lib/pgai/clone_manager.rb +20 -78
- data/lib/pgai/dblab.rb +78 -0
- data/lib/pgai/port_forward.rb +66 -0
- data/lib/pgai/version.rb +1 -1
- data/lib/pgai.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ec24e1bc611522aea3c490acbb0f94b02085584544c979cd56ae9a3bd45f61b
|
4
|
+
data.tar.gz: 7351d32aa09341ec0081e0d4c45466d83034d1ec3ec776b74bef7e05c455997e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82761cb5d303b13edc9a8b9541e11a9c5d2c06b3765879ede4d6cf4176a8d11f1bcd24563da88481ff6648b308f265f8cc0a2b56eb01ac2cd5888417ab485062
|
7
|
+
data.tar.gz: 527e1976b374a68409c6ca8591b6ab6cb043ed374f69a3daa87d76116bed7d0d04d518d00d6df38bf1305965f9db71cdf4e8aac9c2e4e01fa9a31aa7a00723be
|
data/README.md
CHANGED
@@ -22,10 +22,24 @@ Before usage `pgai config` must be executed and at least an environment must be
|
|
22
22
|
|
23
23
|
An access token will be required and it can be obtained from: https://console.postgres.ai/gitlab/tokens
|
24
24
|
|
25
|
-
|
25
|
+
Example:
|
26
26
|
|
27
|
+
```shell
|
28
|
+
pgai config --dbname=gitlabhq_dblab --prefix=<gitlab handle> --proxy=<domain> --exe=<optional /path/to/dblab>
|
29
|
+
```
|
30
|
+
|
31
|
+
To configure environments:
|
32
|
+
|
33
|
+
```shell
|
34
|
+
pgai env add --alias ci --id ci-database --port 12345
|
27
35
|
```
|
28
|
-
|
36
|
+
|
37
|
+
The environment id, port, and proxy domain can be found by clicking on the `Connect` button on a database instance page.
|
38
|
+
|
39
|
+
Configuring the user and identity file for the proxy domain must be done using the `~/.ssh/config` file. Example:
|
40
|
+
|
41
|
+
```
|
42
|
+
Host <domain>
|
29
43
|
IdentityFile ~/.ssh/id_ed25519
|
30
44
|
User <username>
|
31
45
|
```
|
@@ -48,6 +62,13 @@ pgai connect <env alias>
|
|
48
62
|
|
49
63
|
Multiple `connect` commands for an environment will connect to the same clone, it won't start a new one.
|
50
64
|
|
65
|
+
### Features
|
66
|
+
|
67
|
+
- multiple psql sessions to the same clone
|
68
|
+
- multiple environments support
|
69
|
+
- automatic port forward management
|
70
|
+
- prevents system sleep while psql sessions are active via `caffeinate`
|
71
|
+
|
51
72
|
## Contributing
|
52
73
|
|
53
74
|
Bug reports and pull requests are welcome on GitLab at https://gitlab.com/mbobin/pgai.
|
data/lib/pgai/clone_manager.rb
CHANGED
@@ -1,23 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require "socket"
|
5
|
-
require "json"
|
6
|
-
require "securerandom"
|
7
|
-
require "net/http"
|
8
|
-
require "pathname"
|
9
|
-
require "fileutils"
|
3
|
+
require 'securerandom'
|
10
4
|
|
11
5
|
module Pgai
|
12
6
|
class CloneManager
|
13
7
|
HOSTNAME = "127.0.0.1"
|
14
|
-
DEFAULT_DBLAB = Pathname.new("~/.dblab/dblab").expand_path
|
15
|
-
DBLAB_RELEASE_CHANNEL = "master"
|
16
|
-
DBLAB_BINARY_URL = "https://storage.googleapis.com/database-lab-cli/#{DBLAB_RELEASE_CHANNEL}/dblab-%{os}-%{cpu}"
|
17
8
|
|
18
9
|
def initialize(environment, config:)
|
19
10
|
@environment = environment
|
20
11
|
@config = config
|
12
|
+
@port_forward = PortForward.new(config: config, hostname: HOSTNAME)
|
13
|
+
@dblab = Dblab.new(config: config, hostname: HOSTNAME)
|
21
14
|
end
|
22
15
|
|
23
16
|
def connect
|
@@ -30,20 +23,18 @@ module Pgai
|
|
30
23
|
configure_enviroment
|
31
24
|
return unless find_raw_clone
|
32
25
|
|
33
|
-
dblab(
|
26
|
+
dblab.destroy_clone(id: clone_id)
|
34
27
|
config.remove_clone(clone_id)
|
35
|
-
|
28
|
+
port_forward.stop
|
36
29
|
end
|
37
30
|
|
38
31
|
private
|
39
32
|
|
40
|
-
attr_reader :environment, :config
|
33
|
+
attr_reader :environment, :config, :port_forward, :dblab
|
41
34
|
|
42
35
|
def configure_enviroment
|
43
|
-
|
44
|
-
|
45
|
-
dblab("init", "--url", "http://#{HOSTNAME}:#{enviroment_port}", "--token", config.access_token, "--environment-id", environment_id)
|
46
|
-
dblab("config", "switch", environment_id, raw: true)
|
36
|
+
port_forward.start(enviroment_port)
|
37
|
+
dblab.configure_env(port: enviroment_port, token: config.access_token, id: environment_id)
|
47
38
|
end
|
48
39
|
|
49
40
|
def find_or_create_clone
|
@@ -61,12 +52,11 @@ module Pgai
|
|
61
52
|
end
|
62
53
|
|
63
54
|
def find_raw_clone
|
64
|
-
|
55
|
+
dblab.list_clones.find { |clone| clone["id"] == clone_id }
|
65
56
|
end
|
66
57
|
|
67
58
|
def create_clone
|
68
|
-
raw_clone = dblab(
|
69
|
-
raise "Could not create clone" unless raw_clone
|
59
|
+
raw_clone = dblab.create_clone(id: clone_id, user: clone_user, password: clone_password)
|
70
60
|
|
71
61
|
attributes = {
|
72
62
|
port: raw_clone.dig("db", "port"),
|
@@ -80,14 +70,18 @@ module Pgai
|
|
80
70
|
end
|
81
71
|
|
82
72
|
def psql(clone)
|
83
|
-
|
73
|
+
port_forward.start(clone.port)
|
84
74
|
|
85
75
|
psql_pid = fork do
|
86
|
-
wait_for_connections(clone.port)
|
87
76
|
exec("psql #{clone.connection_string}")
|
88
77
|
end
|
89
78
|
|
79
|
+
Signal.trap("INT") { }
|
80
|
+
start_caffeinate(psql_pid)
|
90
81
|
Process.wait(psql_pid)
|
82
|
+
ensure
|
83
|
+
port_forward.conditionally_stop(clone.port, [psql_pid])
|
84
|
+
port_forward.conditionally_stop(enviroment_port)
|
91
85
|
end
|
92
86
|
|
93
87
|
def environment_id
|
@@ -110,63 +104,11 @@ module Pgai
|
|
110
104
|
@clone_user ||= SecureRandom.hex(16)
|
111
105
|
end
|
112
106
|
|
113
|
-
def
|
114
|
-
|
115
|
-
return output if raw
|
116
|
-
|
117
|
-
JSON.parse(output) unless output.empty?
|
118
|
-
end
|
119
|
-
|
120
|
-
def dblab_path
|
121
|
-
return config.path unless config.path.to_s.empty?
|
122
|
-
return "dblab" unless `which dblab`.to_s.empty?
|
123
|
-
return DEFAULT_DBLAB.to_s if DEFAULT_DBLAB.exist?
|
124
|
-
|
125
|
-
download_dblab_to(DEFAULT_DBLAB)
|
126
|
-
File.chmod(0o755, DEFAULT_DBLAB)
|
127
|
-
|
128
|
-
DEFAULT_DBLAB
|
129
|
-
end
|
130
|
-
|
131
|
-
def download_dblab_to(location)
|
132
|
-
platform = Gem::Platform.local
|
133
|
-
uri = URI(DBLAB_BINARY_URL % {os: platform.os, cpu: platform.cpu})
|
134
|
-
|
135
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
136
|
-
http.request Net::HTTP::Get.new(uri) do |response|
|
137
|
-
FileUtils.mkdir_p File.dirname(location)
|
138
|
-
File.open(location, "w") do |io|
|
139
|
-
response.read_body { |chunk| io.write chunk }
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def start_port_forward(port)
|
146
|
-
return if port_open?(port)
|
147
|
-
|
148
|
-
system("ssh -fNTML #{port}:#{HOSTNAME}:#{port} #{config.proxy}")
|
149
|
-
wait_for_connections(port)
|
150
|
-
end
|
151
|
-
|
152
|
-
def stop_port_forwards
|
153
|
-
raw_pids = `ps ax | grep 'ssh -fNTML' | grep '#{config.proxy}' | grep -v grep | awk '{ print $1 }'`
|
154
|
-
|
155
|
-
raw_pids.split.map do |pid|
|
156
|
-
Process.kill("HUP", pid.to_i)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def port_open?(port)
|
161
|
-
!!TCPSocket.new(HOSTNAME, port)
|
162
|
-
rescue Errno::ECONNREFUSED
|
163
|
-
false
|
164
|
-
end
|
107
|
+
def start_caffeinate(pid)
|
108
|
+
return if `which caffeinate`.to_s.empty?
|
165
109
|
|
166
|
-
|
167
|
-
|
168
|
-
sleep 0.02
|
169
|
-
end
|
110
|
+
caffeinate_pid = Process.spawn("caffeinate -is -w #{pid}")
|
111
|
+
Process.detach(caffeinate_pid)
|
170
112
|
end
|
171
113
|
end
|
172
114
|
end
|
data/lib/pgai/dblab.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shellwords"
|
4
|
+
require "json"
|
5
|
+
require "net/http"
|
6
|
+
require "pathname"
|
7
|
+
require "fileutils"
|
8
|
+
|
9
|
+
module Pgai
|
10
|
+
class Dblab
|
11
|
+
DEFAULT_DBLAB = Pathname.new("~/.dblab/dblab").expand_path
|
12
|
+
DBLAB_RELEASE_CHANNEL = "master"
|
13
|
+
DBLAB_BINARY_URL = "https://storage.googleapis.com/database-lab-cli/#{DBLAB_RELEASE_CHANNEL}/dblab-%{os}-%{cpu}"
|
14
|
+
|
15
|
+
def initialize(config:, hostname:)
|
16
|
+
@config = config
|
17
|
+
@hostname = hostname
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure_env(port:, token:, id:)
|
21
|
+
dblab("init", "--url", "http://#{hostname}:#{port}", "--token", token, "--environment-id", id, silence: true)
|
22
|
+
dblab("config", "switch", id, raw: true)
|
23
|
+
end
|
24
|
+
|
25
|
+
def list_clones
|
26
|
+
Array(dblab("clone", "list"))
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_clone(id:, user:, password:)
|
30
|
+
data = dblab("clone", "create", "--id", id, "--username", user, "--password", password)
|
31
|
+
raise "Could not create clone" unless data
|
32
|
+
|
33
|
+
data
|
34
|
+
end
|
35
|
+
|
36
|
+
def destroy_clone(id:)
|
37
|
+
dblab("clone", "destroy", id, raw: true)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :config, :hostname
|
43
|
+
|
44
|
+
def dblab(*args, raw: false, silence: false)
|
45
|
+
redirect = "2>/dev/null" if silence
|
46
|
+
|
47
|
+
output = `#{args.unshift(dblab_path).shelljoin} #{redirect}`
|
48
|
+
return output if raw
|
49
|
+
|
50
|
+
JSON.parse(output) unless output.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def dblab_path
|
54
|
+
return config.path unless config.path.to_s.empty?
|
55
|
+
return "dblab" unless `which dblab`.to_s.empty?
|
56
|
+
return DEFAULT_DBLAB.to_s if DEFAULT_DBLAB.exist?
|
57
|
+
|
58
|
+
download_dblab_to(DEFAULT_DBLAB)
|
59
|
+
File.chmod(0o755, DEFAULT_DBLAB)
|
60
|
+
|
61
|
+
DEFAULT_DBLAB
|
62
|
+
end
|
63
|
+
|
64
|
+
def download_dblab_to(location)
|
65
|
+
platform = Gem::Platform.local
|
66
|
+
uri = URI(DBLAB_BINARY_URL % {os: platform.os, cpu: platform.cpu})
|
67
|
+
|
68
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
69
|
+
http.request Net::HTTP::Get.new(uri) do |response|
|
70
|
+
FileUtils.mkdir_p File.dirname(location)
|
71
|
+
File.open(location, "w") do |io|
|
72
|
+
response.read_body { |chunk| io.write chunk }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shellwords"
|
4
|
+
require "socket"
|
5
|
+
|
6
|
+
module Pgai
|
7
|
+
class PortForward
|
8
|
+
def initialize(config:, hostname:)
|
9
|
+
@config = config
|
10
|
+
@hostname = hostname
|
11
|
+
end
|
12
|
+
|
13
|
+
def start(port)
|
14
|
+
return if ready?(port)
|
15
|
+
|
16
|
+
system("ssh -fNTML #{port.to_i}:#{escape hostname}:#{port.to_i} #{escape config.proxy}")
|
17
|
+
wait_until_ready(port)
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop(port = nil)
|
21
|
+
port_forward_pids(port).each do |pid|
|
22
|
+
Process.kill("HUP", pid.to_i)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def conditionally_stop(port, ignored_pids = [])
|
27
|
+
pids = lsof(port, ignored_pids + [Process.pid])
|
28
|
+
pf_pids = port_forward_pids(port)
|
29
|
+
|
30
|
+
stop(port) if (pids - pf_pids).empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :config, :hostname
|
36
|
+
|
37
|
+
def ready?(port)
|
38
|
+
!!TCPSocket.new(hostname, port)
|
39
|
+
rescue Errno::ECONNREFUSED
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def wait_until_ready(port)
|
44
|
+
until ready?(port)
|
45
|
+
sleep 0.02
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def port_forward_pids(port_filter = nil)
|
50
|
+
ssh_filter = escape "ssh -fNTML #{port_filter&.to_i}".strip
|
51
|
+
host_filter = escape config.proxy
|
52
|
+
|
53
|
+
`ps ax | grep #{ssh_filter} | grep #{host_filter} | grep -v grep | awk '{ print $1 }'`.split.map(&:to_i)
|
54
|
+
end
|
55
|
+
|
56
|
+
def lsof(port, exclude_pids = [])
|
57
|
+
ignored_pids = exclude_pids.map { |pid| "-p^#{pid.to_i}" }.join(" ")
|
58
|
+
|
59
|
+
`lsof -i:#{port.to_i} #{ignored_pids} -t`.split.map(&:to_i)
|
60
|
+
end
|
61
|
+
|
62
|
+
def escape(...)
|
63
|
+
Shellwords.escape(...)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/pgai/version.rb
CHANGED
data/lib/pgai.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pgai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marius Bobin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-04-
|
11
|
+
date: 2023-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -89,6 +89,8 @@ files:
|
|
89
89
|
- lib/pgai/cli/main.rb
|
90
90
|
- lib/pgai/clone_manager.rb
|
91
91
|
- lib/pgai/config.rb
|
92
|
+
- lib/pgai/dblab.rb
|
93
|
+
- lib/pgai/port_forward.rb
|
92
94
|
- lib/pgai/version.rb
|
93
95
|
homepage: https://gitlab.com/mbobin/pgai
|
94
96
|
licenses:
|