pgchief 0.3.1 → 0.5.0

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: 627e9af2dc140bd7210c9f4e999f5be13ba0bf514e6397619d42bef8a6d8b838
4
- data.tar.gz: 78a5456432854cebd9b6d6fb68f9f02256a9a37c626ac0d78ef35b3bf79f4f21
3
+ metadata.gz: adccab6ee77468761bf0d05d12954f7b0360547b95f5295797b911ed80f731e2
4
+ data.tar.gz: 2a4ae39f43e0da9913f252e70e0583d0e21dd45e48931e94a62c309298d287f8
5
5
  SHA512:
6
- metadata.gz: 26a19391751adbd9846cd70532becfa91c4f2e6a704405764c207e817f388b5ad900a5993eb2e20b574e09e78a089658b9f1f14591e9ea5f0349476fa7b49c13
7
- data.tar.gz: e96db718dd3e594b4f612d47b2d63bfe0792e94b4ba622d9093db6b2d8138c02cf8dc45885557a8048f767d280f597103a410468914496dd937e25b759eb2c18
6
+ metadata.gz: 7dea359e077b5c473c7df21e53067954dee0c426df76b8a375a026a3976593dbdd4024c6582650a2fbe1404a34affb1030fe0065842065ad594119cd3f757ace
7
+ data.tar.gz: 3b16494cea09736f6b2c16e55d2c5054898a6f023bf06b8862585313617104dc858e98750397baf6b606126aaf2f5bb109f9f02fbf11699b281bdeefd6c41c38
data/CHANGELOG.md CHANGED
@@ -13,6 +13,29 @@ and this project will try its best to adhere to [Semantic Versioning](https://se
13
13
 
14
14
  ### Fixes
15
15
 
16
+ ## [0.5.0]
17
+
18
+ ### Additions
19
+
20
+ * Restore database from local file(s)
21
+ * Restore database from s3
22
+
23
+ ## [0.4.0]
24
+
25
+ ### Changes
26
+
27
+ * Clean up the config object
28
+
29
+ ### Additions
30
+
31
+ * Back up option for databases: save to local filesystem or S3.
32
+
33
+ ### Fixes
34
+
35
+ * Capture error where the config file does not exist and provide some guidance.
36
+ * Make a `PG::ConnectionBad` error a little less scary(?)
37
+ * Do not inherit the base `Command` class in `ConfigCreate`. It doesn't need to connect to the DB.
38
+
16
39
  ## [0.3.1]
17
40
 
18
41
  ### Changes
@@ -75,7 +98,9 @@ and this project will try its best to adhere to [Semantic Versioning](https://se
75
98
  - Drop user ✅
76
99
  - List databases ✅
77
100
 
78
- [Unreleased]: https://github.com/jayroh/pgchief/compare/v0.3.0...HEAD
101
+ [Unreleased]: https://github.com/jayroh/pgchief/compare/v0.4.0...HEAD
102
+ [0.4.0]: https://github.com/jayroh/pgchief/releases/tag/v0.4.0
103
+ [0.3.1]: https://github.com/jayroh/pgchief/releases/tag/v0.3.1
79
104
  [0.3.0]: https://github.com/jayroh/pgchief/releases/tag/v0.3.0
80
105
  [0.2.0]: https://github.com/jayroh/pgchief/releases/tag/v0.2.0
81
106
  [0.1.0]: https://github.com/jayroh/pgchief/releases/tag/v0.1.0
data/README.md CHANGED
@@ -41,7 +41,7 @@ pgchief
41
41
 
42
42
  ## Config
43
43
 
44
- Format of `~/.pgchief.toml`
44
+ Format of `~/.config/pgchief/config.toml`
45
45
 
46
46
  ```toml
47
47
  # Connection string to superuser account at your PG instance
@@ -130,5 +130,9 @@ Give "rando-username" access to database(s):
130
130
  * [x] Give user permissions to use database
131
131
  * [x] Initialize toml file
132
132
  * [x] Display connection information
133
- * [ ] Back up database
134
- * [ ] Restore database
133
+ * [x] Back up database locally
134
+ * [x] Back up database to S3
135
+ * [x] Restore local database
136
+ * [x] Restore remote database @ S3
137
+ * [ ] Quickly back up via command line option
138
+ * [ ] Quickly restore via command line option
data/config/pgchief.toml CHANGED
@@ -8,3 +8,9 @@ backup_dir = "~/.config/pgchief/backups"
8
8
 
9
9
  # Location of saved database connection strings
10
10
  # credentials_file = "~/.config/pgchief/credentials"
11
+
12
+ # S3 config - if present, will back up to S3 instead of local filesystem
13
+ # s3_key = ""
14
+ # s3_secret = ""
15
+ # s3_region = "us-east-1"
16
+ # s3_path_prefix = "s3://bucket-name/database-backups/"
@@ -13,6 +13,8 @@ module Pgchief
13
13
  def initialize(*params)
14
14
  @params = params
15
15
  @conn = PG.connect(Pgchief::Config.pgurl)
16
+ rescue PG::ConnectionBad => e
17
+ puts "Cannot connect to database. #{e.message}"
16
18
  end
17
19
 
18
20
  def call
@@ -5,7 +5,11 @@ require "fileutils"
5
5
  module Pgchief
6
6
  module Command
7
7
  # Create a configuration file at $HOME
8
- class ConfigCreate < Base
8
+ class ConfigCreate
9
+ def self.call(dir: "#{Dir.home}/.config/pgchief")
10
+ new.call(dir: dir)
11
+ end
12
+
9
13
  def call(dir: "#{Dir.home}/.config/pgchief")
10
14
  return if File.exist?("#{dir}/config.toml")
11
15
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Pgchief
6
+ module Command
7
+ # Command object to drop a database
8
+ class DatabaseBackup < Base
9
+ extend Forwardable
10
+
11
+ def_delegators :@uploader, :configured?, :upload!, :s3_location
12
+
13
+ attr_reader :database
14
+
15
+ def call
16
+ @database = params.first
17
+ @uploader = Pgchief::Command::S3Upload.new(local_location)
18
+ raise Pgchief::Errors::DatabaseMissingError unless db_exists?
19
+
20
+ backup!
21
+ upload! if configured?
22
+
23
+ "Database '#{database}' backed up to #{location}"
24
+ rescue PG::Error => e
25
+ "Error: #{e.message}"
26
+ ensure
27
+ conn.close
28
+ end
29
+
30
+ private
31
+
32
+ def backup!
33
+ `pg_dump -Fc #{database} -f #{local_location}`
34
+ end
35
+
36
+ def db_exists?
37
+ query = "SELECT 1 FROM pg_database WHERE datname = '#{database}'"
38
+ conn.exec(query).any?
39
+ end
40
+
41
+ def location
42
+ configured? ? s3_location : local_location
43
+ end
44
+
45
+ def local_location
46
+ @local_location ||= begin
47
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
48
+ "#{Pgchief::Config.backup_dir}#{database}-#{timestamp}.dump"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Pgchief
6
+ module Command
7
+ # Command object to restore a database
8
+ class DatabaseRestore < Base
9
+ extend Forwardable
10
+
11
+ attr_reader :database, :filename
12
+
13
+ def_delegators :s3, :configured?, :client, :bucket, :path
14
+
15
+ def call
16
+ @database = params.first
17
+ @filename = params.last
18
+ raise Pgchief::Errors::DatabaseMissingError unless db_exists?
19
+
20
+ download! if configured?
21
+ restore!
22
+
23
+ "Database '#{database}' restored from #{filename}"
24
+ rescue PG::Error => e
25
+ "Error: #{e.message}"
26
+ ensure
27
+ conn.close
28
+ end
29
+
30
+ private
31
+
32
+ def download!
33
+ client.get_object(
34
+ bucket: bucket,
35
+ key: "#{path}#{filename}",
36
+ response_target: local_location
37
+ )
38
+ end
39
+
40
+ def restore!
41
+ `pg_restore --clean --no-owner --dbname=#{Pgchief::Config.pgurl}/#{database} #{local_location}`
42
+ end
43
+
44
+ def db_exists?
45
+ query = "SELECT 1 FROM pg_database WHERE datname = '#{database}'"
46
+ conn.exec(query).any?
47
+ end
48
+
49
+ def local_location
50
+ "#{Pgchief::Config.backup_dir}/#{filename}"
51
+ end
52
+
53
+ def s3
54
+ Pgchief::Config.s3
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgchief
4
+ module Command
5
+ # Class to upload a file to S3
6
+ class S3Upload
7
+ attr_reader :local_location, :file_name, :bucket, :path
8
+
9
+ def initialize(local_location)
10
+ @local_location = local_location
11
+ @file_name = File.basename(local_location)
12
+ end
13
+
14
+ def upload!
15
+ s3.client.put_object(
16
+ bucket: s3.bucket,
17
+ key: "#{s3.path}#{file_name}",
18
+ body: File.open(local_location, "rb"),
19
+ acl: "private",
20
+ content_type: "application/octet-stream"
21
+ )
22
+ end
23
+
24
+ def s3_location
25
+ "s3://#{s3.bucket}/#{s3.path}#{file_name}"
26
+ end
27
+
28
+ def configured?
29
+ s3.configured?
30
+ end
31
+
32
+ private
33
+
34
+ def s3
35
+ Pgchief::Config.s3
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Pgchief
6
+ class Config
7
+ # Class to store s3 configuration settings
8
+ class S3
9
+ extend Forwardable
10
+
11
+ def_delegators \
12
+ :config,
13
+ :s3_key,
14
+ :s3_secret,
15
+ :s3_region,
16
+ :s3_path_prefix
17
+
18
+ PREFIX_REGEX = %r{\As3://(?<bucket>(\w|-)*)/(?<path>(\w|/)*/)\z}
19
+
20
+ attr_reader :config
21
+
22
+ def initialize(config)
23
+ @config = config
24
+ end
25
+
26
+ def client
27
+ @client ||= Aws::S3::Client.new(
28
+ access_key_id: s3_key,
29
+ secret_access_key: s3_secret,
30
+ region: s3_region
31
+ )
32
+ end
33
+
34
+ def bucket
35
+ s3_match[:bucket]
36
+ end
37
+
38
+ def path
39
+ s3_match[:path]
40
+ end
41
+
42
+ def configured?
43
+ [
44
+ s3_key,
45
+ s3_secret,
46
+ s3_region,
47
+ s3_path_prefix
48
+ ].none?(&:nil?)
49
+ end
50
+
51
+ private
52
+
53
+ def s3_match
54
+ @s3_match ||= s3_path_prefix.match(PREFIX_REGEX)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -7,30 +7,63 @@ module Pgchief
7
7
  class Config
8
8
  class << self
9
9
  attr_accessor \
10
- :backup_dir,
11
- :credentials_file
10
+ :s3_key,
11
+ :s3_secret,
12
+ :s3_region
12
13
 
13
14
  attr_writer :pgurl
14
15
 
15
- def load_config!(toml_file = "#{Dir.home}/.config/pgchief/config.toml")
16
- config = TomlRB.load_file(toml_file, symbolize_keys: true)
16
+ attr_reader \
17
+ :s3_path_prefix,
18
+ :backup_dir,
19
+ :credentials_file
20
+
21
+ def load_config!(toml_file = "#{Dir.home}/.config/pgchief/config.toml") # rubocop:disable Metrics/AbcSize
22
+ config = TomlRB.load_file(toml_file, symbolize_keys: true)
23
+ self.backup_dir = config[:backup_dir]
24
+ self.credentials_file = config[:credentials_file]
25
+ self.pgurl = config[:pgurl]
26
+ self.s3_key = config[:s3_key]
27
+ self.s3_secret = config[:s3_secret]
28
+ self.s3_region = config[:s3_region]
29
+ self.s3_path_prefix = config[:s3_path_prefix]
30
+ rescue Errno::ENOENT
31
+ puts config_missing_error(toml_file)
32
+ end
17
33
 
18
- @backup_dir = config[:backup_dir].gsub("~", Dir.home)
19
- @credentials_file = config[:credentials_file]&.gsub("~", Dir.home)
20
- @pgurl = config[:pgurl]
34
+ def s3
35
+ @s3 ||= Pgchief::Config::S3.new(self)
21
36
  end
22
37
 
23
38
  def pgurl
24
39
  ENV.fetch("DATABASE_URL", @pgurl)
25
40
  end
26
41
 
42
+ def backup_dir=(value)
43
+ @backup_dir = value ? "#{value.chomp("/")}/".gsub("~", Dir.home) : "/tmp/"
44
+ end
45
+
46
+ def s3_path_prefix=(value)
47
+ @s3_path_prefix = value ? "#{value.chomp("/")}/" : nil
48
+ end
49
+
50
+ def credentials_file=(value)
51
+ @credentials_file = value&.gsub("~", Dir.home)
52
+ end
53
+
27
54
  def set_up_file_structure!
28
55
  FileUtils.mkdir_p(backup_dir)
29
-
30
56
  return unless credentials_file && !File.exist?(credentials_file)
31
57
 
32
58
  FileUtils.touch(credentials_file)
33
59
  end
60
+
61
+ private
62
+
63
+ def config_missing_error(toml_file)
64
+ "You must create a config file at #{toml_file}.\n" \
65
+ "run `pgchief --init` to create it."
66
+ end
34
67
  end
35
68
  end
36
69
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "pry"
5
+
6
+ module Pgchief
7
+ class Database
8
+ # Get a list of all backups for a given database
9
+ class Backups
10
+ extend Forwardable
11
+
12
+ def_delegators :s3, :bucket, :path, :client
13
+
14
+ def self.for(database, remote)
15
+ new(database, remote).for
16
+ end
17
+
18
+ attr_reader :database, :remote
19
+
20
+ def initialize(database, remote)
21
+ @database = database
22
+ @remote = remote
23
+ end
24
+
25
+ def for
26
+ remote ? remote_backups : local_backups
27
+ end
28
+
29
+ def remote_backups
30
+ @remote_backups ||= client.list_objects(
31
+ bucket: bucket,
32
+ prefix: "#{path}#{database}-"
33
+ ).contents
34
+ .map(&:key)
35
+ .sort
36
+ .last(3)
37
+ .reverse
38
+ .map { |f| File.basename(f) }
39
+ end
40
+
41
+ def local_backups
42
+ Dir["#{Pgchief::Config.backup_dir}#{database}-*.dump"]
43
+ .sort_by { |f| File.mtime(f) }
44
+ .reverse
45
+ .last(3)
46
+ .map { |f| File.basename(f) }
47
+ end
48
+
49
+ private
50
+
51
+ def s3
52
+ Pgchief::Config.s3
53
+ end
54
+ end
55
+ end
56
+ end
@@ -14,5 +14,9 @@ module Pgchief
14
14
  ensure
15
15
  conn.close
16
16
  end
17
+
18
+ def self.backups_for(database, remote: false)
19
+ Pgchief::Database::Backups.for(database, remote: remote)
20
+ end
17
21
  end
18
22
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgchief
4
+ module Prompt
5
+ # Class to prompt for which database to backup
6
+ class BackupDatabase < Base
7
+ def call
8
+ database = prompt.select("Which database needs backing up?", Pgchief::Database.all)
9
+ result = Pgchief::Command::DatabaseBackup.call(database)
10
+
11
+ prompt.say result
12
+ end
13
+ end
14
+ end
15
+ end
@@ -6,8 +6,14 @@ module Pgchief
6
6
  class DatabaseManagement < Base
7
7
  def call
8
8
  prompt = TTY::Prompt.new
9
- result = prompt.select("Database management", ["Create database", "Drop database", "Database List"])
10
- scope = result == "Database List" ? "command" : "prompt"
9
+ result = prompt.select("Database management", [
10
+ "Create database",
11
+ "Drop database",
12
+ "Database List",
13
+ "Backup database",
14
+ "Restore database"
15
+ ])
16
+ scope = result == "Database List" ? "command" : "prompt"
11
17
 
12
18
  klassify(scope, result).call
13
19
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgchief
4
+ module Prompt
5
+ # Class to prompt for which database to restore
6
+ class RestoreDatabase < Base
7
+ def call
8
+ database = prompt.select("Which database needs restoring?", Pgchief::Database.all)
9
+ local_file = prompt.select("Which backup file do you want to restore?", Pgchief::Database.backups_for(database))
10
+ result = Pgchief::Command::DatabaseRestore.call(database, local_file)
11
+
12
+ prompt.say result
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgchief
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/pgchief.rb CHANGED
@@ -3,33 +3,41 @@
3
3
  require "pg"
4
4
  require "tty-prompt"
5
5
  require "tty-option"
6
+ require "aws-sdk-s3"
6
7
 
7
8
  require "pgchief/cli"
8
9
  require "pgchief/config"
10
+ require "pgchief/config/s3"
9
11
  require "pgchief/connection_string"
10
12
  require "pgchief/version"
11
13
  require "pgchief/database"
14
+ require "pgchief/database/backups"
12
15
  require "pgchief/user"
13
16
 
14
17
  require "pgchief/prompt/base"
15
- require "pgchief/prompt/start"
18
+ require "pgchief/prompt/backup_database"
16
19
  require "pgchief/prompt/create_database"
17
20
  require "pgchief/prompt/create_user"
18
21
  require "pgchief/prompt/database_management"
19
22
  require "pgchief/prompt/drop_database"
20
23
  require "pgchief/prompt/drop_user"
21
- require "pgchief/prompt/user_management"
22
24
  require "pgchief/prompt/grant_database_privileges"
25
+ require "pgchief/prompt/restore_database"
26
+ require "pgchief/prompt/start"
27
+ require "pgchief/prompt/user_management"
23
28
  require "pgchief/prompt/view_database_connection_string"
24
29
 
25
30
  require "pgchief/command"
26
31
  require "pgchief/command/base"
27
32
  require "pgchief/command/config_create"
33
+ require "pgchief/command/database_backup"
28
34
  require "pgchief/command/database_create"
29
35
  require "pgchief/command/database_drop"
30
36
  require "pgchief/command/database_list"
31
37
  require "pgchief/command/database_privileges_grant"
38
+ require "pgchief/command/database_restore"
32
39
  require "pgchief/command/retrieve_connection_string"
40
+ require "pgchief/command/s3_upload"
33
41
  require "pgchief/command/store_connection_string"
34
42
  require "pgchief/command/user_create"
35
43
  require "pgchief/command/user_drop"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgchief
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Oliveira
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-12 00:00:00.000000000 Z
11
+ date: 2024-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-s3
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: pg
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -89,19 +103,25 @@ files:
89
103
  - lib/pgchief/command.rb
90
104
  - lib/pgchief/command/base.rb
91
105
  - lib/pgchief/command/config_create.rb
106
+ - lib/pgchief/command/database_backup.rb
92
107
  - lib/pgchief/command/database_create.rb
93
108
  - lib/pgchief/command/database_drop.rb
94
109
  - lib/pgchief/command/database_list.rb
95
110
  - lib/pgchief/command/database_privileges_grant.rb
111
+ - lib/pgchief/command/database_restore.rb
96
112
  - lib/pgchief/command/retrieve_connection_string.rb
113
+ - lib/pgchief/command/s3_upload.rb
97
114
  - lib/pgchief/command/store_connection_string.rb
98
115
  - lib/pgchief/command/user_create.rb
99
116
  - lib/pgchief/command/user_drop.rb
100
117
  - lib/pgchief/command/user_list.rb
101
118
  - lib/pgchief/config.rb
119
+ - lib/pgchief/config/s3.rb
102
120
  - lib/pgchief/connection_string.rb
103
121
  - lib/pgchief/database.rb
122
+ - lib/pgchief/database/backups.rb
104
123
  - lib/pgchief/prompt.rb
124
+ - lib/pgchief/prompt/backup_database.rb
105
125
  - lib/pgchief/prompt/base.rb
106
126
  - lib/pgchief/prompt/create_database.rb
107
127
  - lib/pgchief/prompt/create_user.rb
@@ -109,6 +129,7 @@ files:
109
129
  - lib/pgchief/prompt/drop_database.rb
110
130
  - lib/pgchief/prompt/drop_user.rb
111
131
  - lib/pgchief/prompt/grant_database_privileges.rb
132
+ - lib/pgchief/prompt/restore_database.rb
112
133
  - lib/pgchief/prompt/start.rb
113
134
  - lib/pgchief/prompt/user_management.rb
114
135
  - lib/pgchief/prompt/view_database_connection_string.rb
@@ -139,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
160
  - !ruby/object:Gem::Version
140
161
  version: '0'
141
162
  requirements: []
142
- rubygems_version: 3.2.33
163
+ rubygems_version: 3.5.21
143
164
  signing_key:
144
165
  specification_version: 4
145
166
  summary: A simple ruby script to manage postgresql databases and users