pgai 0.1.3 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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: