pgchief 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 756ccf173c3d0cbe56bb969e2f7d20e9032e1a7b1c600155e212548fa81f93bb
4
- data.tar.gz: c00f369d5e6013bedb955daba82da8c54ad2a77c978b2ead411cae3ef0ca5f0f
3
+ metadata.gz: df59b4bbe15540e5d3b587f98c98ef71f2724f028299e88961766e2b6225ac2c
4
+ data.tar.gz: f5b0f578ac0c0610e2f020f740f207826a1f4cd05de16e27e5da7c1af860a408
5
5
  SHA512:
6
- metadata.gz: 5ec3b0a81250bd7c8b479780ec658adf5a51f7f19db08c0447f1470ba4477cee24c7a00affec84ba5db0cf136a18e02de636e1d2f6701f0b6aa13d3ffee88afa
7
- data.tar.gz: 828bf9aebb8235af34a0462f2d2890e5d51245e98841c97644a1d4411ba9147d5f9a1b594c2367f0dd34a96dff47f45201d0d312e8b862e90f27f83b5aad4514
6
+ metadata.gz: f2e9611feb5db3d76d8e13ca3b153d1a0a387c90faadd249590c115b94d696da5d0b4d5b712db6d7a739b8f9b933fea1f40f9e6dc102fb064098b6ff3b568f58
7
+ data.tar.gz: 863b4f71e694fb3e8ebde4d4b020d446d96a582a24b4e457d0d8cd42bc474780956d874a02944ea959ac689a3d33870374565871c19deea2bc31c750349aff8a
data/CHANGELOG.md CHANGED
@@ -13,6 +13,35 @@ and this project will try its best to adhere to [Semantic Versioning](https://se
13
13
 
14
14
  ### Fixes
15
15
 
16
+ ## [0.4.0]
17
+
18
+ ### Changes
19
+
20
+ * Clean up the config object
21
+
22
+ ### Additions
23
+
24
+ * Back up option for databases: save to local filesystem or S3.
25
+
26
+ ### Fixes
27
+
28
+ * Capture error where the config file does not exist and provide some guidance.
29
+ * Make a `PG::ConnectionBad` error a little less scary(?)
30
+ * Do not inherit the base `Command` class in `ConfigCreate`. It doesn't need to connect to the DB.
31
+
32
+ ## [0.3.1]
33
+
34
+ ### Changes
35
+
36
+ - The main database url will be accessed via the config class and will prioritize
37
+ the ENV over the config file, if it is set.
38
+
39
+ ### Fixes
40
+
41
+ - Do not use the ENV only. Until now that was the only method and it was ignoring
42
+ what was set in the config file. This fixes the issue where nothing will work
43
+ if `ENV["DATABASE_URL"]` is not set.
44
+
16
45
  ## [0.3.0]
17
46
 
18
47
  ### Additions
@@ -62,7 +91,9 @@ and this project will try its best to adhere to [Semantic Versioning](https://se
62
91
  - Drop user ✅
63
92
  - List databases ✅
64
93
 
65
- [Unreleased]: https://github.com/jayroh/pgchief/compare/v0.3.0...HEAD
94
+ [Unreleased]: https://github.com/jayroh/pgchief/compare/v0.4.0...HEAD
95
+ [0.4.0]: https://github.com/jayroh/pgchief/releases/tag/v0.4.0
96
+ [0.3.1]: https://github.com/jayroh/pgchief/releases/tag/v0.3.1
66
97
  [0.3.0]: https://github.com/jayroh/pgchief/releases/tag/v0.3.0
67
98
  [0.2.0]: https://github.com/jayroh/pgchief/releases/tag/v0.2.0
68
99
  [0.1.0]: https://github.com/jayroh/pgchief/releases/tag/v0.1.0
data/README.md CHANGED
@@ -26,19 +26,22 @@ below for the feature check-list and current progress.
26
26
  ```sh
27
27
  gem install pgchief
28
28
 
29
- # To initialize the config file at `~/.pgchief.toml`:
29
+ # To initialize the config file at `~/.config/pgchief/config.toml`:
30
30
 
31
31
  pgchief --init
32
32
 
33
- # make sure the DATABASE_URL is set to the connection string for a pg server's superuser
34
- export DATABASE_URL=postgresql://postgres:password@postgres.local:5432
33
+ # edit the config file and set your main administrative connection string
34
+ # (vim|nano|pico|ed) ~/.config/pgchief/config.toml
35
+
36
+ # OR ... make sure the DATABASE_URL env is set to the connection string
37
+ # export DATABASE_URL=postgresql://postgres:password@postgres.local:5432
35
38
 
36
39
  pgchief
37
40
  ```
38
41
 
39
42
  ## Config
40
43
 
41
- Format of `~/.pgchief.toml`
44
+ Format of `~/.config/pgchief/config.toml`
42
45
 
43
46
  ```toml
44
47
  # Connection string to superuser account at your PG instance
@@ -127,5 +130,9 @@ Give "rando-username" access to database(s):
127
130
  * [x] Give user permissions to use database
128
131
  * [x] Initialize toml file
129
132
  * [x] Display connection information
130
- * [ ] Back up database
131
- * [ ] Restore database
133
+ * [x] Back up database locally
134
+ * [x] Back up database to S3
135
+ * [ ] Restore local database
136
+ * [ ] 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/"
@@ -12,7 +12,9 @@ module Pgchief
12
12
 
13
13
  def initialize(*params)
14
14
  @params = params
15
- @conn = PG.connect(Pgchief::DATABASE_URL)
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
@@ -25,7 +25,7 @@ module Pgchief
25
25
  private
26
26
 
27
27
  def grant_privs! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
28
- conn = PG.connect("#{Pgchief::DATABASE_URL}/#{database}")
28
+ conn = PG.connect("#{Pgchief::Config.pgurl}/#{database}")
29
29
  conn.exec("GRANT CONNECT ON DATABASE #{database} TO #{username};")
30
30
  conn.exec("GRANT CREATE ON SCHEMA public TO #{username};")
31
31
  conn.exec("GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO #{username};")
@@ -50,7 +50,7 @@ module Pgchief
50
50
 
51
51
  def connection_string
52
52
  ConnectionString.new(
53
- Pgchief::DATABASE_URL,
53
+ Pgchief::Config.pgurl,
54
54
  username: username,
55
55
  password: password,
56
56
  database: database
@@ -0,0 +1,40 @@
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
+ FileUtils.rm(local_location)
23
+ end
24
+
25
+ def s3_location
26
+ "s3://#{s3.bucket}/#{s3.path}#{file_name}"
27
+ end
28
+
29
+ def configured?
30
+ s3.configured?
31
+ end
32
+
33
+ private
34
+
35
+ def s3
36
+ Pgchief::Config.s3
37
+ end
38
+ end
39
+ end
40
+ end
@@ -49,7 +49,7 @@ module Pgchief
49
49
 
50
50
  def connection_string
51
51
  ConnectionString.new(
52
- Pgchief::DATABASE_URL,
52
+ Pgchief::Config.pgurl,
53
53
  username: username,
54
54
  password: password
55
55
  ).to_s
@@ -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,25 +7,63 @@ module Pgchief
7
7
  class Config
8
8
  class << self
9
9
  attr_accessor \
10
+ :s3_key,
11
+ :s3_secret,
12
+ :s3_region
13
+
14
+ attr_writer :pgurl
15
+
16
+ attr_reader \
17
+ :s3_path_prefix,
10
18
  :backup_dir,
11
- :credentials_file,
12
- :pgurl
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
13
33
 
14
- def load_config!(toml_file = "#{Dir.home}/.config/pgchief/config.toml")
15
- config = TomlRB.load_file(toml_file, symbolize_keys: true)
34
+ def s3
35
+ @s3 ||= Pgchief::Config::S3.new(self)
36
+ end
37
+
38
+ def pgurl
39
+ ENV.fetch("DATABASE_URL", @pgurl)
40
+ end
16
41
 
17
- @backup_dir = config[:backup_dir].gsub("~", Dir.home)
18
- @credentials_file = config[:credentials_file]&.gsub("~", Dir.home)
19
- @pgurl = config[:pgurl]
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)
20
52
  end
21
53
 
22
54
  def set_up_file_structure!
23
55
  FileUtils.mkdir_p(backup_dir)
24
-
25
56
  return unless credentials_file && !File.exist?(credentials_file)
26
57
 
27
58
  FileUtils.touch(credentials_file)
28
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
29
67
  end
30
68
  end
31
69
  end
@@ -6,7 +6,7 @@ module Pgchief
6
6
  # Database information and operations
7
7
  class Database
8
8
  def self.all
9
- conn = PG.connect(Pgchief::DATABASE_URL)
9
+ conn = PG.connect(Pgchief::Config.pgurl)
10
10
  result = conn.exec("SELECT datname FROM pg_database WHERE datistemplate = false")
11
11
  result
12
12
  .map { |row| row["datname"] }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgchief
4
+ module Prompt
5
+ # Class to prompt for which database to drop
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,13 @@ 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
+ ])
15
+ scope = result == "Database List" ? "command" : "prompt"
11
16
 
12
17
  klassify(scope, result).call
13
18
  end
data/lib/pgchief/user.rb CHANGED
@@ -6,7 +6,7 @@ module Pgchief
6
6
  # Database information and operations
7
7
  class User
8
8
  def self.all
9
- conn = PG.connect(Pgchief::DATABASE_URL)
9
+ conn = PG.connect(Pgchief::Config.pgurl)
10
10
  result = conn.exec("SELECT usename FROM pg_user")
11
11
 
12
12
  result.map { |row| row["usename"] }.reject { |name| name == "postgres" }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgchief
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/pgchief.rb CHANGED
@@ -3,9 +3,11 @@
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"
@@ -13,6 +15,7 @@ require "pgchief/user"
13
15
 
14
16
  require "pgchief/prompt/base"
15
17
  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"
@@ -25,19 +28,19 @@ require "pgchief/prompt/view_database_connection_string"
25
28
  require "pgchief/command"
26
29
  require "pgchief/command/base"
27
30
  require "pgchief/command/config_create"
31
+ require "pgchief/command/database_backup"
28
32
  require "pgchief/command/database_create"
29
33
  require "pgchief/command/database_drop"
30
34
  require "pgchief/command/database_list"
31
35
  require "pgchief/command/database_privileges_grant"
32
36
  require "pgchief/command/retrieve_connection_string"
37
+ require "pgchief/command/s3_upload"
33
38
  require "pgchief/command/store_connection_string"
34
39
  require "pgchief/command/user_create"
35
40
  require "pgchief/command/user_drop"
36
41
  require "pgchief/command/user_list"
37
42
 
38
43
  module Pgchief
39
- DATABASE_URL = ENV.fetch("DATABASE_URL")
40
-
41
44
  class Error < StandardError; end
42
45
 
43
46
  module Errors
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.0
4
+ version: 0.4.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-11 00:00:00.000000000 Z
11
+ date: 2024-11-27 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
@@ -79,7 +93,6 @@ files:
79
93
  - ".rubocop.yml"
80
94
  - CHANGELOG.md
81
95
  - CODE_OF_CONDUCT.md
82
- - LICENSE
83
96
  - LICENSE.txt
84
97
  - README.md
85
98
  - Rakefile
@@ -90,19 +103,23 @@ files:
90
103
  - lib/pgchief/command.rb
91
104
  - lib/pgchief/command/base.rb
92
105
  - lib/pgchief/command/config_create.rb
106
+ - lib/pgchief/command/database_backup.rb
93
107
  - lib/pgchief/command/database_create.rb
94
108
  - lib/pgchief/command/database_drop.rb
95
109
  - lib/pgchief/command/database_list.rb
96
110
  - lib/pgchief/command/database_privileges_grant.rb
97
111
  - lib/pgchief/command/retrieve_connection_string.rb
112
+ - lib/pgchief/command/s3_upload.rb
98
113
  - lib/pgchief/command/store_connection_string.rb
99
114
  - lib/pgchief/command/user_create.rb
100
115
  - lib/pgchief/command/user_drop.rb
101
116
  - lib/pgchief/command/user_list.rb
102
117
  - lib/pgchief/config.rb
118
+ - lib/pgchief/config/s3.rb
103
119
  - lib/pgchief/connection_string.rb
104
120
  - lib/pgchief/database.rb
105
121
  - lib/pgchief/prompt.rb
122
+ - lib/pgchief/prompt/backup_database.rb
106
123
  - lib/pgchief/prompt/base.rb
107
124
  - lib/pgchief/prompt/create_database.rb
108
125
  - lib/pgchief/prompt/create_user.rb
data/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Joel Oliveira
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.