pgai 0.1.4 → 0.1.6

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: 6ec24e1bc611522aea3c490acbb0f94b02085584544c979cd56ae9a3bd45f61b
4
+ data.tar.gz: 7351d32aa09341ec0081e0d4c45466d83034d1ec3ec776b74bef7e05c455997e
5
5
  SHA512:
6
- metadata.gz: 90a083cc81d4c2b55892f9d1137aefa7dbb7bda601b2a058a42e6011a630eac7e4469ec586363a334a95c795955c7809d4b285937c49ba099e4761f16b7fb6a9
7
- data.tar.gz: 0c1dcc4947700235508cd9ea3ffbcececa52f32b98c1d04c3f931ae7d97b72ef334c1409b2fd70c86a0867990445dbd4288b8cb9028ce9a910030d40bc51733c
6
+ metadata.gz: 82761cb5d303b13edc9a8b9541e11a9c5d2c06b3765879ede4d6cf4176a8d11f1bcd24563da88481ff6648b308f265f8cc0a2b56eb01ac2cd5888417ab485062
7
+ data.tar.gz: 527e1976b374a68409c6ca8591b6ab6cb043ed374f69a3daa87d76116bed7d0d04d518d00d6df38bf1305965f9db71cdf4e8aac9c2e4e01fa9a31aa7a00723be
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,16 @@
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"
3
+ require 'securerandom'
10
4
 
11
5
  module Pgai
12
6
  class CloneManager
13
7
  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
8
 
18
9
  def initialize(environment, config:)
19
10
  @environment = environment
20
11
  @config = config
12
+ @port_forward = PortForward.new(config: config, hostname: HOSTNAME)
13
+ @dblab = Dblab.new(config: config, hostname: HOSTNAME)
21
14
  end
22
15
 
23
16
  def connect
@@ -30,20 +23,18 @@ module Pgai
30
23
  configure_enviroment
31
24
  return unless find_raw_clone
32
25
 
33
- dblab("clone", "destroy", clone_id, raw: true)
26
+ dblab.destroy_clone(id: clone_id)
34
27
  config.remove_clone(clone_id)
35
- stop_port_forwards
28
+ port_forward.stop
36
29
  end
37
30
 
38
31
  private
39
32
 
40
- attr_reader :environment, :config
33
+ attr_reader :environment, :config, :port_forward, :dblab
41
34
 
42
35
  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)
36
+ port_forward.start(enviroment_port)
37
+ dblab.configure_env(port: enviroment_port, token: config.access_token, id: environment_id)
47
38
  end
48
39
 
49
40
  def find_or_create_clone
@@ -61,12 +52,11 @@ module Pgai
61
52
  end
62
53
 
63
54
  def find_raw_clone
64
- Array(dblab("clone", "list")).find { |clone| clone["id"] == clone_id }
55
+ dblab.list_clones.find { |clone| clone["id"] == clone_id }
65
56
  end
66
57
 
67
58
  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
59
+ raw_clone = dblab.create_clone(id: clone_id, user: clone_user, password: clone_password)
70
60
 
71
61
  attributes = {
72
62
  port: raw_clone.dig("db", "port"),
@@ -80,14 +70,18 @@ module Pgai
80
70
  end
81
71
 
82
72
  def psql(clone)
83
- start_port_forward(clone.port)
73
+ port_forward.start(clone.port)
84
74
 
85
75
  psql_pid = fork do
86
- wait_for_connections(clone.port)
87
76
  exec("psql #{clone.connection_string}")
88
77
  end
89
78
 
79
+ Signal.trap("INT") { }
80
+ start_caffeinate(psql_pid)
90
81
  Process.wait(psql_pid)
82
+ ensure
83
+ port_forward.conditionally_stop(clone.port, [psql_pid])
84
+ port_forward.conditionally_stop(enviroment_port)
91
85
  end
92
86
 
93
87
  def environment_id
@@ -110,63 +104,11 @@ module Pgai
110
104
  @clone_user ||= SecureRandom.hex(16)
111
105
  end
112
106
 
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 }'`
154
-
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
107
+ def start_caffeinate(pid)
108
+ return if `which caffeinate`.to_s.empty?
165
109
 
166
- def wait_for_connections(port)
167
- until port_open?(port)
168
- sleep 0.02
169
- end
110
+ caffeinate_pid = Process.spawn("caffeinate -is -w #{pid}")
111
+ Process.detach(caffeinate_pid)
170
112
  end
171
113
  end
172
114
  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.6"
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.6
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-27 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: