pgai 0.2.4 → 1.0.0

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.
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Pgai
6
+ module Resources
7
+ module Remote
8
+ class DbObject < BaseRecord
9
+ attribute :username, :string, default: -> { SecureRandom.hex(8) }
10
+ attribute :password, :string, default: -> { SecureRandom.hex(16) }
11
+ attribute :restricted, :boolean, default: false
12
+ attribute :host, :string
13
+ attribute :port, :integer
14
+ attribute :db_name, :string
15
+ attribute :conn_str, :string
16
+
17
+ def refresh_attributes(data)
18
+ super(data.except(:username, :password))
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgai
4
+ module Resources
5
+ module Remote
6
+ class Snapshot < BaseRecord
7
+ attribute :id, :string
8
+ attribute :created_at, :datetime
9
+ attribute :data_state_at, :datetime
10
+ attribute :physical_size, :integer
11
+ attribute :logical_size, :integer
12
+ attribute :pool, :string
13
+ attribute :num_clones, :integer
14
+
15
+ class << self
16
+ def all
17
+ client.get(path).map { new(_1) }
18
+ end
19
+
20
+ def latest
21
+ all.max_by(&:created_at)
22
+ end
23
+
24
+ def path
25
+ "snapshots"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/pgai/store.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "pstore"
5
+ require "fileutils"
6
+
7
+ module Pgai
8
+ class Store
9
+ STORE_PATH = "~/.config/pgai/config.pstore"
10
+
11
+ def all(record_type)
12
+ store.transaction(true) do
13
+ store[record_type]&.values || {}
14
+ end
15
+ end
16
+
17
+ def find(record_type, id)
18
+ store.transaction(true) do
19
+ (store[record_type] || {})[id]
20
+ end
21
+ end
22
+
23
+ def delete(record_type, id)
24
+ store.transaction do
25
+ store[record_type] ||= {}
26
+ store[record_type] = store[record_type].except(id)
27
+ end
28
+ end
29
+
30
+ def save(record_type, attributes, key: :id)
31
+ store.transaction do
32
+ store[record_type] ||= {}
33
+ store[record_type].merge!(attributes[key] => attributes)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def store
40
+ @store ||= PStore.new(store_path)
41
+ end
42
+
43
+ def store_path
44
+ @store_path ||= Pathname(STORE_PATH).expand_path.tap do |path|
45
+ FileUtils.mkdir_p File.dirname(path)
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/pgai/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pgai
2
- VERSION = "0.2.4"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/pgai.rb CHANGED
@@ -1,16 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "pgai/version"
4
- require_relative "pgai/config"
5
- require_relative "pgai/dblab"
6
- require_relative "pgai/port_forward"
7
- require_relative "pgai/clone_manager"
8
- require_relative "pgai/cli/base"
9
- require_relative "pgai/cli/env"
10
- require_relative "pgai/cli/main"
3
+ require "zeitwerk"
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.setup
11
7
 
12
8
  module Pgai
13
9
  class Error < StandardError; end
14
10
 
15
- class CloneNotFount < Error; end
11
+ class ResourceNotFound < Error; end
16
12
  end
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.2.4
4
+ version: 1.0.0
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-05-29 00:00:00.000000000 Z
11
+ date: 2024-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -30,6 +30,148 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 1.2.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: zeitwerk
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.6.13
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '2.6'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.6.13
53
+ - !ruby/object:Gem::Dependency
54
+ name: excon
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: 0.109.0
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: 0.109.0
67
+ - !ruby/object:Gem::Dependency
68
+ name: net-ssh
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '7.2'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 7.2.1
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '7.2'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 7.2.1
87
+ - !ruby/object:Gem::Dependency
88
+ name: ed25519
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '1.2'
94
+ - - "<"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
97
+ type: :runtime
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '1.2'
104
+ - - "<"
105
+ - !ruby/object:Gem::Version
106
+ version: '2.0'
107
+ - !ruby/object:Gem::Dependency
108
+ name: bcrypt_pbkdf
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '1.0'
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: '2.0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '1.0'
124
+ - - "<"
125
+ - !ruby/object:Gem::Version
126
+ version: '2.0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: tty-progressbar
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: 0.18.2
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: 0.18.2
141
+ - !ruby/object:Gem::Dependency
142
+ name: tty-logger
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: 0.6.0
148
+ type: :runtime
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: 0.6.0
155
+ - !ruby/object:Gem::Dependency
156
+ name: concurrent-ruby
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: '1.2'
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: 1.2.3
165
+ type: :runtime
166
+ prerelease: false
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - "~>"
170
+ - !ruby/object:Gem::Version
171
+ version: '1.2'
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: 1.2.3
33
175
  - !ruby/object:Gem::Dependency
34
176
  name: rake
35
177
  requirement: !ruby/object:Gem::Requirement
@@ -72,6 +214,20 @@ dependencies:
72
214
  - - "~>"
73
215
  - !ruby/object:Gem::Version
74
216
  version: '1.3'
217
+ - !ruby/object:Gem::Dependency
218
+ name: simplecov
219
+ requirement: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - "~>"
222
+ - !ruby/object:Gem::Version
223
+ version: 0.22.0
224
+ type: :development
225
+ prerelease: false
226
+ version_requirements: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - "~>"
229
+ - !ruby/object:Gem::Version
230
+ version: 0.22.0
75
231
  description:
76
232
  email:
77
233
  - mbobin@gitlab.com
@@ -87,10 +243,27 @@ files:
87
243
  - lib/pgai/cli/base.rb
88
244
  - lib/pgai/cli/env.rb
89
245
  - lib/pgai/cli/main.rb
246
+ - lib/pgai/client.rb
90
247
  - lib/pgai/clone_manager.rb
91
- - lib/pgai/config.rb
92
- - lib/pgai/dblab.rb
93
- - lib/pgai/port_forward.rb
248
+ - lib/pgai/commander.rb
249
+ - lib/pgai/create_clone_service.rb
250
+ - lib/pgai/external_command_manager.rb
251
+ - lib/pgai/port/allocator.rb
252
+ - lib/pgai/port/forwarder.rb
253
+ - lib/pgai/port/manager.rb
254
+ - lib/pgai/psql_manager.rb
255
+ - lib/pgai/resources/attributes.rb
256
+ - lib/pgai/resources/local/base_record.rb
257
+ - lib/pgai/resources/local/clone.rb
258
+ - lib/pgai/resources/local/configuration.rb
259
+ - lib/pgai/resources/local/environment.rb
260
+ - lib/pgai/resources/remote/base_record.rb
261
+ - lib/pgai/resources/remote/clone.rb
262
+ - lib/pgai/resources/remote/clone_metadata.rb
263
+ - lib/pgai/resources/remote/clone_status.rb
264
+ - lib/pgai/resources/remote/db_object.rb
265
+ - lib/pgai/resources/remote/snapshot.rb
266
+ - lib/pgai/store.rb
94
267
  - lib/pgai/version.rb
95
268
  homepage: https://gitlab.com/mbobin/pgai
96
269
  licenses:
@@ -111,7 +284,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
284
  - !ruby/object:Gem::Version
112
285
  version: '0'
113
286
  requirements: []
114
- rubygems_version: 3.4.11
287
+ rubygems_version: 3.4.10
115
288
  signing_key:
116
289
  specification_version: 4
117
290
  summary: CLI wrapper for postgres.ai thin clones
data/lib/pgai/config.rb DELETED
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pathname"
4
- require "pstore"
5
- require "fileutils"
6
-
7
- module Pgai
8
- class Config
9
- Clone = Struct.new(:host, :port, :password, :user, :dbname, :created_at, :data_state_at, keyword_init: true) do
10
- def connection_string
11
- "'host=#{host} port=#{port} user=#{user} dbname=#{dbname} password=#{password}'"
12
- end
13
-
14
- def database_url
15
- "postgresql://#{user}:#{password}@#{host}:#{port}/#{dbname}"
16
- end
17
- end
18
-
19
- attr_accessor :clone_prefix
20
- attr_accessor :dbname
21
- attr_accessor :access_token
22
- attr_accessor :proxy
23
- attr_accessor :path
24
-
25
- def self.load
26
- new { |config| config.load_from_store }
27
- end
28
-
29
- def self.persist(data)
30
- new { |config| config.write(data) }
31
- end
32
-
33
- def initialize
34
- yield self if block_given?
35
- end
36
-
37
- def load_from_store
38
- store.transaction(true) do
39
- self.clone_prefix = store[:clone_prefix]
40
- self.dbname = store[:dbname]
41
- self.access_token = store[:access_token]
42
- self.proxy = store[:proxy]
43
- self.path = store[:path]
44
- end
45
- end
46
-
47
- def write(options)
48
- store.transaction do
49
- store[:clone_prefix] = options[:prefix]
50
- store[:dbname] = options[:dbname]
51
- store[:access_token] = options[:token]
52
- store[:proxy] = options[:proxy]
53
- store[:path] = options[:path]
54
- store[:clones] = {}
55
- end
56
- end
57
-
58
- def find_clone(id)
59
- store.transaction(true) do
60
- clones = store[:clones] || {}
61
-
62
- Clone.new(clones[id]) if clones[id]
63
- end
64
- end
65
-
66
- def remove_clone(id)
67
- store.transaction do
68
- store[:clones] ||= {}
69
- store[:clones] = store[:clones].except(id)
70
- end
71
- end
72
-
73
- def persist_clone(id, attributes = {})
74
- store.transaction do
75
- store[:clones] ||= {}
76
- store[:clones].merge!(id => attributes)
77
- end
78
-
79
- Clone.new(attributes)
80
- end
81
-
82
- def add_env(options = {})
83
- store.transaction do
84
- store[:environments] ||= {}
85
- store[:environments].merge!(options[:alias] => options)
86
- end
87
- end
88
-
89
- def remove_env(env_alias)
90
- store.transaction do
91
- store[:environments] ||= {}
92
- store[:environments] = store[:environments].except(env_alias)
93
- end
94
- end
95
-
96
- def enviroments
97
- store.transaction(true) do
98
- store[:environments] || {}
99
- end
100
- end
101
-
102
- private
103
-
104
- def store
105
- @store ||= PStore.new(store_path)
106
- end
107
-
108
- def store_path
109
- @store_path ||= Pathname("~/.config/pgai/config.pstore").expand_path.tap do |path|
110
- FileUtils.mkdir_p File.dirname(path)
111
- end
112
- end
113
- end
114
- end
data/lib/pgai/dblab.rb DELETED
@@ -1,91 +0,0 @@
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, raw: 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
- def reset_clone(id:)
41
- dblab("clone", "reset", id, raw: true)
42
- end
43
-
44
- private
45
-
46
- attr_reader :config, :hostname
47
-
48
- def dblab(*args, raw: false, silence: false)
49
- redirect = "2>/dev/null" if silence
50
-
51
- output = `#{args.unshift(dblab_path).shelljoin} #{redirect}`
52
- return output if raw
53
-
54
- JSON.parse(output) unless output.empty?
55
- end
56
-
57
- def dblab_path
58
- return config.path unless config.path.to_s.empty?
59
- return "dblab" unless `which dblab`.to_s.empty?
60
- return DEFAULT_DBLAB.to_s if DEFAULT_DBLAB.exist?
61
-
62
- download_dblab_to(DEFAULT_DBLAB)
63
- File.chmod(0o755, DEFAULT_DBLAB)
64
-
65
- DEFAULT_DBLAB
66
- end
67
-
68
- def download_dblab_to(location)
69
- uri = URI(DBLAB_BINARY_URL % {os: platform.os, cpu: cpu_platform})
70
-
71
- Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
72
- http.request Net::HTTP::Get.new(uri) do |response|
73
- FileUtils.mkdir_p File.dirname(location)
74
- File.open(location, "w") do |io|
75
- response.read_body { |chunk| io.write chunk }
76
- end
77
- end
78
- end
79
- end
80
-
81
- def platform
82
- Gem::Platform.local
83
- end
84
-
85
- def cpu_platform
86
- cpu = platform.cpu
87
- return "arm64" if cpu == "aarch64"
88
- cpu
89
- end
90
- end
91
- end
@@ -1,66 +0,0 @@
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