pgai 0.1.4 → 0.1.5
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 +4 -4
- data/README.md +23 -2
- data/lib/pgai/clone_manager.rb +18 -80
- 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: a31b2fc8c0e25d189cd3f3191a8e1833de2cbe8f21dfe5e4322793fea1aa4efd
|
4
|
+
data.tar.gz: 6061e6095d92912eb7d5a2f64fe95740314494ff23aefab32afe4e9b15d77d3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee38e79b9be45e6a848659a830550ce0cf7ad3cf272fc2c6d6e9ddb95214595ae9fd506a4726d3fffe36719e56a4d4bf439ff8543099ee457feed13e5a6ebbc6
|
7
|
+
data.tar.gz: b3d34902baa84fc445e10d1874455b3168fe3896c78fd83d41b84c24d78ee1bdc9ccf02282b984d848cdf2984bc8f4ca136b43ff367c9a0857857d164b554358
|
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,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "shellwords"
|
4
|
-
require "socket"
|
5
|
-
require "json"
|
6
|
-
require "securerandom"
|
7
|
-
require "net/http"
|
8
|
-
require "pathname"
|
9
|
-
require "fileutils"
|
10
|
-
|
11
3
|
module Pgai
|
12
4
|
class CloneManager
|
13
5
|
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
6
|
|
18
7
|
def initialize(environment, config:)
|
19
8
|
@environment = environment
|
20
9
|
@config = config
|
10
|
+
@port_forward = PortForward.new(config: config, hostname: HOSTNAME)
|
11
|
+
@dblab = Dblab.new(config: config, hostname: HOSTNAME)
|
21
12
|
end
|
22
13
|
|
23
14
|
def connect
|
@@ -30,20 +21,18 @@ module Pgai
|
|
30
21
|
configure_enviroment
|
31
22
|
return unless find_raw_clone
|
32
23
|
|
33
|
-
dblab(
|
24
|
+
dblab.destroy_clone(id: clone_id)
|
34
25
|
config.remove_clone(clone_id)
|
35
|
-
|
26
|
+
port_forward.stop
|
36
27
|
end
|
37
28
|
|
38
29
|
private
|
39
30
|
|
40
|
-
attr_reader :environment, :config
|
31
|
+
attr_reader :environment, :config, :port_forward, :dblab
|
41
32
|
|
42
33
|
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)
|
34
|
+
port_forward.start(enviroment_port)
|
35
|
+
dblab.configure_env(port: enviroment_port, token: config.access_token, id: environment_id)
|
47
36
|
end
|
48
37
|
|
49
38
|
def find_or_create_clone
|
@@ -61,12 +50,11 @@ module Pgai
|
|
61
50
|
end
|
62
51
|
|
63
52
|
def find_raw_clone
|
64
|
-
|
53
|
+
dblab.list_clones.find { |clone| clone["id"] == clone_id }
|
65
54
|
end
|
66
55
|
|
67
56
|
def create_clone
|
68
|
-
raw_clone = dblab(
|
69
|
-
raise "Could not create clone" unless raw_clone
|
57
|
+
raw_clone = dblab.create_clone(id: clone_id, user: clone_user, password: clone_password)
|
70
58
|
|
71
59
|
attributes = {
|
72
60
|
port: raw_clone.dig("db", "port"),
|
@@ -80,14 +68,16 @@ module Pgai
|
|
80
68
|
end
|
81
69
|
|
82
70
|
def psql(clone)
|
83
|
-
|
71
|
+
port_forward.start(clone.port)
|
84
72
|
|
85
73
|
psql_pid = fork do
|
86
|
-
|
74
|
+
start_caffeinate(Process.pid)
|
87
75
|
exec("psql #{clone.connection_string}")
|
88
76
|
end
|
89
|
-
|
90
77
|
Process.wait(psql_pid)
|
78
|
+
ensure
|
79
|
+
port_forward.conditionally_stop(clone.port, [psql_pid])
|
80
|
+
port_forward.conditionally_stop(enviroment_port)
|
91
81
|
end
|
92
82
|
|
93
83
|
def environment_id
|
@@ -110,63 +100,11 @@ module Pgai
|
|
110
100
|
@clone_user ||= SecureRandom.hex(16)
|
111
101
|
end
|
112
102
|
|
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 }'`
|
103
|
+
def start_caffeinate(pid)
|
104
|
+
return if `which caffeinate`.to_s.empty?
|
154
105
|
|
155
|
-
|
156
|
-
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def port_open?(port)
|
161
|
-
!!TCPSocket.new(HOSTNAME, port)
|
162
|
-
rescue Errno::ECONNREFUSED
|
163
|
-
false
|
164
|
-
end
|
165
|
-
|
166
|
-
def wait_for_connections(port)
|
167
|
-
until port_open?(port)
|
168
|
-
sleep 0.02
|
169
|
-
end
|
106
|
+
caffeinate_pid = Process.spawn("caffeinate -is -w #{pid}")
|
107
|
+
Process.detach(caffeinate_pid)
|
170
108
|
end
|
171
109
|
end
|
172
110
|
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.5
|
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-25 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:
|