pgai 0.2.4 → 1.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.alpha2"
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.alpha2
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-01 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:
@@ -107,11 +280,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
280
  version: 3.0.0
108
281
  required_rubygems_version: !ruby/object:Gem::Requirement
109
282
  requirements:
110
- - - ">="
283
+ - - ">"
111
284
  - !ruby/object:Gem::Version
112
- version: '0'
285
+ version: 1.3.1
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