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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29e67c364882e415161c298f0463f6e3850a7ea057b8256a3a4e17643d4a8694
4
- data.tar.gz: bdcce4986037ace414698eeae62ce3f73f33084ace78e99e778a2b55975f5ad3
3
+ metadata.gz: a31b2fc8c0e25d189cd3f3191a8e1833de2cbe8f21dfe5e4322793fea1aa4efd
4
+ data.tar.gz: 6061e6095d92912eb7d5a2f64fe95740314494ff23aefab32afe4e9b15d77d3b
5
5
  SHA512:
6
- metadata.gz: 90a083cc81d4c2b55892f9d1137aefa7dbb7bda601b2a058a42e6011a630eac7e4469ec586363a334a95c795955c7809d4b285937c49ba099e4761f16b7fb6a9
7
- data.tar.gz: 0c1dcc4947700235508cd9ea3ffbcececa52f32b98c1d04c3f931ae7d97b72ef334c1409b2fd70c86a0867990445dbd4288b8cb9028ce9a910030d40bc51733c
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
- Configuring the user and identity file for the proxy must be done using the `~/.ssh/config` file. Example:
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
- Host <subdomain>.postgres.ai
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.
@@ -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("clone", "destroy", clone_id, raw: true)
24
+ dblab.destroy_clone(id: clone_id)
34
25
  config.remove_clone(clone_id)
35
- stop_port_forwards
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
- start_port_forward(enviroment_port)
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
- Array(dblab("clone", "list")).find { |clone| clone["id"] == clone_id }
53
+ dblab.list_clones.find { |clone| clone["id"] == clone_id }
65
54
  end
66
55
 
67
56
  def create_clone
68
- raw_clone = dblab("clone", "create", "--id", clone_id, "--username", clone_user, "--password", clone_password)
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
- start_port_forward(clone.port)
71
+ port_forward.start(clone.port)
84
72
 
85
73
  psql_pid = fork do
86
- wait_for_connections(clone.port)
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 dblab(*args, raw: false)
114
- output = `#{args.unshift(dblab_path).shelljoin}`
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
- 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
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
@@ -1,3 +1,3 @@
1
1
  module Pgai
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
data/lib/pgai.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative "pgai/version"
4
4
  require_relative "pgai/config"
5
+ require_relative "pgai/dblab"
6
+ require_relative "pgai/port_forward"
5
7
  require_relative "pgai/clone_manager"
6
8
  require_relative "pgai/cli/base"
7
9
  require_relative "pgai/cli/env"
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
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-12 00:00:00.000000000 Z
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: