pgai 0.1.4 → 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: 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: