pgai 0.1.3 → 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: 4ffc13ccc5cc54a852ba48b245a1f4e470696d622fe499aa5fbce8221e77fa5d
4
- data.tar.gz: 6e78ffd141aed55d189bab7c986c11434b8d22fbe4c40b032ddc2074875909fb
3
+ metadata.gz: a31b2fc8c0e25d189cd3f3191a8e1833de2cbe8f21dfe5e4322793fea1aa4efd
4
+ data.tar.gz: 6061e6095d92912eb7d5a2f64fe95740314494ff23aefab32afe4e9b15d77d3b
5
5
  SHA512:
6
- metadata.gz: f5457effb587f8f4aeb75b5887cc15c977c736a37459de877ab18c18d8c0fd76e215300f7d46322e56913c398c2e869ec0c461c992aa231ab3b38e3b8bf45b9e
7
- data.tar.gz: 4f29dc64c0553db229c009917b2676e9374ddb16b82c79e608588a9be0a655d10e804b2d8ec0566030a6d732a7ccb53c4681e3f53f65a3cb9ba4fcc59e0b3412
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,22 +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_BINARY_URL = "https://gitlab.com/api/v4/projects/45068363/jobs/4092515399/artifacts/engine/bin/cli/dblab-%{os}-%{cpu}"
16
6
 
17
7
  def initialize(environment, config:)
18
8
  @environment = environment
19
9
  @config = config
10
+ @port_forward = PortForward.new(config: config, hostname: HOSTNAME)
11
+ @dblab = Dblab.new(config: config, hostname: HOSTNAME)
20
12
  end
21
13
 
22
14
  def connect
@@ -29,20 +21,18 @@ module Pgai
29
21
  configure_enviroment
30
22
  return unless find_raw_clone
31
23
 
32
- dblab("clone", "destroy", clone_id, raw: true)
24
+ dblab.destroy_clone(id: clone_id)
33
25
  config.remove_clone(clone_id)
34
- stop_port_forwards
26
+ port_forward.stop
35
27
  end
36
28
 
37
29
  private
38
30
 
39
- attr_reader :environment, :config
31
+ attr_reader :environment, :config, :port_forward, :dblab
40
32
 
41
33
  def configure_enviroment
42
- start_port_forward(enviroment_port)
43
-
44
- dblab("init", "--url", "http://#{HOSTNAME}:#{enviroment_port}", "--token", config.access_token, "--environment-id", environment_id)
45
- 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)
46
36
  end
47
37
 
48
38
  def find_or_create_clone
@@ -60,12 +50,11 @@ module Pgai
60
50
  end
61
51
 
62
52
  def find_raw_clone
63
- Array(dblab("clone", "list")).find { |clone| clone["id"] == clone_id }
53
+ dblab.list_clones.find { |clone| clone["id"] == clone_id }
64
54
  end
65
55
 
66
56
  def create_clone
67
- raw_clone = dblab("clone", "create", "--id", clone_id, "--username", clone_user, "--password", clone_password)
68
- raise "Could not create clone" unless raw_clone
57
+ raw_clone = dblab.create_clone(id: clone_id, user: clone_user, password: clone_password)
69
58
 
70
59
  attributes = {
71
60
  port: raw_clone.dig("db", "port"),
@@ -79,14 +68,16 @@ module Pgai
79
68
  end
80
69
 
81
70
  def psql(clone)
82
- start_port_forward(clone.port)
71
+ port_forward.start(clone.port)
83
72
 
84
73
  psql_pid = fork do
85
- wait_for_connections(clone.port)
74
+ start_caffeinate(Process.pid)
86
75
  exec("psql #{clone.connection_string}")
87
76
  end
88
-
89
77
  Process.wait(psql_pid)
78
+ ensure
79
+ port_forward.conditionally_stop(clone.port, [psql_pid])
80
+ port_forward.conditionally_stop(enviroment_port)
90
81
  end
91
82
 
92
83
  def environment_id
@@ -109,63 +100,11 @@ module Pgai
109
100
  @clone_user ||= SecureRandom.hex(16)
110
101
  end
111
102
 
112
- def dblab(*args, raw: false)
113
- output = `#{args.unshift(dblab_path).shelljoin}`
114
- return output if raw
115
-
116
- JSON.parse(output) unless output.empty?
117
- end
118
-
119
- def dblab_path
120
- return config.path unless config.path.to_s.empty?
121
- return "dblab" unless `which dblab`.to_s.empty?
122
- return DEFAULT_DBLAB.to_s if DEFAULT_DBLAB.exist?
123
-
124
- download_dblab_to(DEFAULT_DBLAB)
125
- File.chmod(0o755, DEFAULT_DBLAB)
126
-
127
- DEFAULT_DBLAB
128
- end
129
-
130
- def download_dblab_to(location)
131
- platform = Gem::Platform.local
132
- uri = URI(DBLAB_BINARY_URL % {os: platform.os, cpu: platform.cpu})
133
-
134
- Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
135
- http.request Net::HTTP::Get.new(uri) do |response|
136
- FileUtils.mkdir_p File.dirname(location)
137
- File.open(location, "w") do |io|
138
- response.read_body { |chunk| io.write chunk }
139
- end
140
- end
141
- end
142
- end
143
-
144
- def start_port_forward(port)
145
- return if port_open?(port)
146
-
147
- system("ssh -fNTML #{port}:#{HOSTNAME}:#{port} #{config.proxy}")
148
- wait_for_connections(port)
149
- end
150
-
151
- def stop_port_forwards
152
- 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?
153
105
 
154
- raw_pids.split.map do |pid|
155
- Process.kill("HUP", pid.to_i)
156
- end
157
- end
158
-
159
- def port_open?(port)
160
- !!TCPSocket.new(HOSTNAME, port)
161
- rescue Errno::ECONNREFUSED
162
- false
163
- end
164
-
165
- def wait_for_connections(port)
166
- until port_open?(port)
167
- sleep 0.02
168
- end
106
+ caffeinate_pid = Process.spawn("caffeinate -is -w #{pid}")
107
+ Process.detach(caffeinate_pid)
169
108
  end
170
109
  end
171
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.3"
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.3
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 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: