bamboolab_kaboom 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b6131743c73049e741f8e78b6523a920f7f43a169f268d7dee816a35b1cbfe5c
4
+ data.tar.gz: 0df5be6710bde734916305e1a9402ef83a040120cce5ede56430f289c8ce77f4
5
+ SHA512:
6
+ metadata.gz: 60d8e4c9ce9a98bcbcf2a52bf75f068d785c5e7192700fd51b95ce22eae1e013eb99dc1d128dc206a438fb1d8a50d156f2003abe10cc3ff8170a386bf82fb87a
7
+ data.tar.gz: 5a5400c999c25135c3fb711df4805b2ce42eb6876a56ddc37805b4ee6728c7106879b4c48ee87d74042dea1917e2b58bab33558d5e6ba56e94e3a0c2c5d8b124
data/bin/kaboom ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'kaboom'
4
+ Kaboom::CLI.start(ARGV)
@@ -0,0 +1,141 @@
1
+ require 'google/cloud/storage'
2
+ require 'fileutils'
3
+ require 'tty-progressbar'
4
+
5
+ module Kaboom
6
+ class Backup < Thor
7
+ include Thor::Actions
8
+ include Utils
9
+ include Config
10
+
11
+ desc 'pull', 'Pull the latest assets and database backup from the deploy destination'
12
+ def pull(destination = 'staging')
13
+ database_pull(destination)
14
+ puts "\n"
15
+ assets_pull(destination)
16
+ say "Completed!", :green
17
+ end
18
+
19
+ desc 'apply', 'Apply a local backup to the database'
20
+ method_option :file, aliases: '-f', type: :string, desc: 'Backup file in to apply', default: 'db/latest.sql'
21
+ def apply
22
+ backup_file = options[:file]
23
+ unless File.exist?(backup_file)
24
+ say_and_exit "Error: Backup file #{backup_file} not found", :red
25
+ end
26
+ confirm_proceed("Do you want to apply the backup to your local database? THIS WILL REMOVE YOUR EXISTING LOCAL DATABASE")
27
+
28
+ local_db_config = database_config('development')
29
+ if DatabaseHelper.apply_backup(local_db_config, backup_file)
30
+ say "Backup applied successfully to local database!", :green
31
+ else
32
+ say_and_exit \
33
+ "Error: Backup restore failed. Is there an open connection to the local database? Stop the Rails application and try again.",
34
+ :red
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def database_pull(destination)
41
+ validate_repository!
42
+ validate_destination(destination)
43
+ confirm_production_action('database backup pull') if destination == 'production'
44
+
45
+ load_env(destination)
46
+ db_config = database_config(destination)
47
+ connection_string = DatabaseHelper.connection_string(db_config)
48
+
49
+ database_size = DatabaseHelper.database_size(connection_string, destination)
50
+
51
+ if confirm_proceed("Database backup size will be around #{database_size}. Do you want to continue?", exit_on_no: false)
52
+ backup_file = ask_for_backup_filename
53
+
54
+ say "Started pulling backup from #{destination} | #{DateTime.now.strftime("%H:%M:%S")}"
55
+ if DatabaseHelper.pull_backup(connection_string, destination, backup_file)
56
+ say "Backup pulled successfully to db/#{backup_file}.sql | #{DateTime.now.strftime("%H:%M:%S")}", :green
57
+ else
58
+ say_and_exit "Error: Backup pull failed", :red
59
+ end
60
+
61
+ if confirm_proceed("Do you want to apply the backup to your local database? THIS WILL REMOVE YOUR EXISTING LOCAL DATABASE")
62
+ say "Applying backup to local database...", :white
63
+ local_db_config = database_config('development')
64
+ if DatabaseHelper.apply_backup(local_db_config, "db/#{backup_file}.sql")
65
+ say "Backup applied successfully to local database!", :green
66
+ else
67
+ say_and_exit \
68
+ "Error: Backup restore failed. Is there an open connection to the local database? Stop the Rails application and try again.",
69
+ :red
70
+
71
+ end
72
+
73
+ remove_backup_file(backup_file) if confirm_proceed("Do you want to remove the local backup file?", exit_on_no: false)
74
+ end
75
+ end
76
+ end
77
+
78
+ def assets_pull(destination)
79
+ validate_repository!
80
+ validate_destination(destination)
81
+ confirm_production_action('assets pull') if destination == 'production'
82
+
83
+ if confirm_proceed("Do you want to pull assets from #{destination}? Your local uploads folder will be deleted.")
84
+ credentials = load_credentials(destination)
85
+ say_and_exit("Error: Google Storage credentials not found in #{destination}.yml", :red) if credentials[:google_storage].nil?
86
+
87
+ project_name = credentials[:google_storage][:project_name]
88
+ bucket_name = credentials[:google_storage][:bucket_name]
89
+
90
+ File.open('.keyfile.json', 'w'){ |f| f.write(credentials[:google_storage][:json_key_string]) }
91
+ storage = Google::Cloud::Storage.new(
92
+ project_id: project_name,
93
+ credentials: ".keyfile.json", # It apsolutely needs to be a file, for some reason
94
+ )
95
+ FileUtils.rm('.keyfile.json')
96
+
97
+ bucket = storage.bucket(bucket_name, skip_lookup: true)
98
+
99
+ files_number = bucket.files(prefix: 'uploads/').all(request_limit: 10_000).count
100
+ if confirm_proceed("#{files_number} files will be downloaded. Do you want to continue?", exit_on_no: false)
101
+ say "WARNING: There is no way to estimate the total size of all assets, make sure you have enough disk space!", :yellow
102
+ say "WARNING: This will take a while, depending on the number of files", :yellow
103
+ say "Started pulling assets from #{destination} | #{DateTime.now.strftime("%H:%M:%S")}", :white
104
+
105
+ FileUtils.rm_rf('public/uploads')
106
+ FileUtils.mkdir_p('public/uploads')
107
+ local_folder = 'public/uploads'
108
+
109
+ progress_bar = TTY::ProgressBar.new("Downloading [:bar] :current/:total files", total: files_number, width: 60)
110
+
111
+ bucket = storage.bucket(bucket_name, skip_lookup: true)
112
+ bucket.files(prefix: 'uploads/').all(request_limit: 1000) do |file|
113
+ local_file_path = File.join(local_folder, file.name.sub('uploads/', ''))
114
+ FileUtils.mkdir_p(File.dirname(local_file_path))
115
+
116
+ file.download(local_file_path)
117
+
118
+ progress_bar.advance
119
+ end
120
+
121
+ say "Assets pulled successfully to #{local_folder} | #{DateTime.now.strftime("%H:%M:%S")}", :green
122
+ end
123
+ end
124
+ end
125
+
126
+ def ask_for_backup_filename
127
+ prompt = TTY::Prompt.new
128
+ prompt.ask("Enter the backup filename:", required: true) do |q|
129
+ q.modify :trim
130
+ q.validate(/^[\w\-. ]+$/, "Filename must contain only valid characters (letters, digits, dashes, underscores, periods, and spaces)")
131
+ q.default 'latest'
132
+ end
133
+ end
134
+
135
+ def remove_backup_file(backup_file)
136
+ File.delete("db/#{backup_file}.sql")
137
+ say "Local backup file removed", :white
138
+ end
139
+
140
+ end
141
+ end
@@ -0,0 +1,86 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'active_support/encrypted_file'
4
+ require 'base64'
5
+ require 'yaml'
6
+ require 'openssl'
7
+ require 'bundler'
8
+
9
+ module Kaboom
10
+ module Config
11
+ include Thor::Actions
12
+ include Utils
13
+
14
+ DESTINATIONS = Dir.glob("./config/deploy.*.yml").map { |file| file.match(/deploy\.(\w+)\.yml/)[1].to_sym }.freeze
15
+
16
+ def validate_repository!
17
+ unless Dir.exist?('.git')
18
+ say_and_exit "Error: This command must be run in a Git repository", :red
19
+ end
20
+
21
+ unless File.exist?('Gemfile') && File.exist?('config/application.rb') && File.exist?('config/routes.rb')
22
+ say_and_exit "Error: This command must be run in a root folder of a Rails application", :red
23
+ end
24
+
25
+ unless File.exist?('config/credentials/development.key')
26
+ say_and_exit "Error: development.key credentials file not found (source it from 1Password)", :red
27
+ end
28
+
29
+ if DESTINATIONS.empty? && !File.exist?('./config/deploy.yml')
30
+ say_and_exit "Error: No deploy configuration files found in config directory.", :red
31
+ end
32
+
33
+ if Bundler::Dsl.evaluate('Gemfile', nil, {})&.ruby_version&.versions&.first != RUBY_VERSION
34
+ say_and_exit "Error: Ruby version in current projects Gemfile is not the same as the currently running Ruby version.", :red
35
+ end
36
+ end
37
+
38
+ def load_env(destination)
39
+ env_file = ".env.#{destination}"
40
+ if File.exist?(env_file)
41
+ ENV.update(Dotenv::Environment.new(env_file))
42
+ else
43
+
44
+ say_and_exit "Environment file #{env_file} not found, run 'kamal envify -d destination'", :red
45
+ end
46
+
47
+ destination_clear_envs = YAML.load_file("config/deploy.#{destination}.yml")['env']['clear']
48
+ ENV.update(destination_clear_envs.transform_values(&:to_s))
49
+ end
50
+
51
+ def load_credentials(destination)
52
+ credentials_key_file = "config/credentials/#{destination}.key"
53
+ credentials_file = "config/credentials/#{destination}.yml.enc"
54
+
55
+ unless File.exist?(credentials_file) && File.exist?(credentials_key_file)
56
+ say_and_exit "Credentials file #{credentials_file} not found, make sure it is present", :red
57
+ end
58
+
59
+ key = [File.read(credentials_key_file)].pack("H*")
60
+ encrypted_credentials = File.binread(credentials_file)
61
+
62
+ cipher = OpenSSL::Cipher.new('aes-128-gcm')
63
+ encrypted_data, iv, auth_tag = encrypted_credentials.split("--".freeze).map { |v| Base64.strict_decode64(v) }
64
+
65
+ cipher.decrypt
66
+ cipher.key = key
67
+ cipher.iv = iv
68
+ cipher.auth_tag = auth_tag
69
+ cipher.auth_data = ""
70
+
71
+ decrypted_data = cipher.update(encrypted_data)
72
+ decrypted_data << cipher.final
73
+
74
+ return YAML.safe_load(Marshal.load(decrypted_data), symbolize_names: true)
75
+ end
76
+
77
+ def database_config(destination)
78
+ config = YAML.load_file('config/database.yml')[destination]
79
+ {
80
+ name: ERB.new(config['database']).result,
81
+ username: ERB.new(config['username']).result.presence || 'rails',
82
+ password: ERB.new(config['password']).result
83
+ }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,33 @@
1
+ module Kaboom
2
+ class DatabaseHelper
3
+ def self.connection_string(config)
4
+ "postgresql://#{config[:username]}:#{config[:password]}@127.0.0.2/#{config[:name]}"
5
+ end
6
+
7
+ def self.database_size(connection_string, destination)
8
+ query = "SELECT pg_size_pretty(pg_database_size(current_database()));"
9
+ command = %Q{kamal accessory exec db --reuse -i -q "psql #{connection_string} -c \\"#{query}\\"" -d #{destination} 2>/dev/null}
10
+ result = `#{command}`.strip
11
+ result.match(/\n\s*(.+?)\s*\n\(/) ? $1 : "Unknown MB"
12
+ end
13
+
14
+ def self.pull_backup(connection_string, destination, backup_file)
15
+ command = %Q{kamal accessory exec db --reuse -i -q "pg_dump #{connection_string}" -d #{destination} > db/#{backup_file}.sql 2>/dev/null}
16
+ system(command)
17
+ end
18
+
19
+ def self.apply_backup(config, backup_file)
20
+ local_connection_string = "postgresql://#{config[:username]}:#{config[:password]}@localhost/template1"
21
+
22
+ commands = [
23
+ %Q{psql #{local_connection_string} -c "DROP DATABASE IF EXISTS #{config[:name]}" >/dev/null 2>&1},
24
+ %Q{psql #{local_connection_string} -c "CREATE DATABASE #{config[:name]};" >/dev/null 2>&1},
25
+ %Q{psql #{local_connection_string.gsub('template1', config[:name])} < #{backup_file} >/dev/null 2>&1},
26
+ %Q{RAILS_MASTER_KEY=`cat config/credentials/development.key` env bin/rails db:environment:set RAILS_ENV=development >/dev/null 2>&1},
27
+ %Q{psql #{local_connection_string} -c "ALTER DATABASE #{config[:name]} OWNER TO #{config[:username]};" >/dev/null 2>&1},
28
+ ]
29
+ #require 'pry'; binding.pry
30
+ commands.all? { |cmd| system(cmd) }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module Kaboom
2
+ module Utils
3
+ def say_and_exit(message, color)
4
+ say message, color
5
+ exit(1)
6
+ end
7
+
8
+ def validate_destination(destination)
9
+ unless Config::DESTINATIONS.include?(destination.to_sym)
10
+ say_and_exit("Error: Unknown destination '#{destination}', valid destinations are: #{Config::DESTINATIONS.join(', ')}", :red)
11
+ end
12
+ end
13
+
14
+ def confirm_production_action(command)
15
+ confirm_proceed("You are about to run a #{command} on a production environment. Are you sure you want to continue?")
16
+ end
17
+
18
+ def confirm_proceed(message, exit_on_no: true)
19
+ yes?(message + " (y/n):", :yellow) || (exit_on_no ? (say("Action aborted", :red) && exit(1)) : false)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaboom
4
+ VERSION = "0.1.0"
5
+ end
data/lib/kaboom.rb ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "kaboom/version"
4
+ require 'yaml'
5
+ require 'erb'
6
+ require 'dotenv'
7
+ require 'thor'
8
+ require 'tty-prompt'
9
+ require 'kamal'
10
+ require 'google/cloud/storage'
11
+
12
+ require_relative 'kaboom/helpers/utils'
13
+ require_relative 'kaboom/helpers/config'
14
+ require_relative 'kaboom/helpers/database_helper'
15
+
16
+ require_relative 'kaboom/backup'
17
+
18
+ module Kaboom
19
+ class Error < StandardError; end
20
+
21
+ class CLI < Thor
22
+ include Thor::Actions
23
+ include Utils
24
+ include Config
25
+
26
+ def self.exit_on_failure?
27
+ true
28
+ end
29
+
30
+ desc 'backup', 'Run database backup/restore related commands'
31
+ subcommand 'backup', Kaboom::Backup
32
+
33
+ desc 'console DESTINATION', 'Run Rails console for deployed apps, defaults to staging'
34
+ method_option :role, aliases: '-r', type: :string, desc: 'Role to run the console at (web, sidekiq, cron...)', default: 'web'
35
+ def console(destination = 'staging')
36
+ validate_repository!
37
+ validate_destination(destination)
38
+
39
+ confirm_production_action('console') if destination == 'production'
40
+ run_command("bundle exec rails c", destination)
41
+ end
42
+
43
+ desc 'ssh DESTINATION', 'Run a bash SSH for deployed apps, defaults to staging'
44
+ method_option :role, aliases: '-r', type: :string, desc: 'Role to run the SSH at (web, sidekiq, cron...)', default: 'web'
45
+ def ssh(destination = 'staging')
46
+ validate_repository!
47
+ validate_destination(destination)
48
+
49
+ confirm_production_action('SSH') if destination == 'production'
50
+ run_command("bash", destination)
51
+ end
52
+
53
+ private
54
+
55
+ def run_command(command, destination)
56
+ say "Running #{command} on #{destination}...", :green
57
+ exec("kamal app exec -i --reuse -q '#{command}' -d #{destination} --roles=#{options[:role]}")
58
+ end
59
+
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bamboolab_kaboom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Your Name
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: kamal
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-prompt
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yaml
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: erb
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: google-cloud-storage
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: tty-progressbar
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: activesupport
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '6.0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '6.0'
153
+ description: Kaboom is a CLI tool to handle app maintenence, SSH connections, database
154
+ and assets backups for apps deployed by Kamal.
155
+ email:
156
+ - your.email@example.com
157
+ executables:
158
+ - kaboom
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - bin/kaboom
163
+ - lib/kaboom.rb
164
+ - lib/kaboom/backup.rb
165
+ - lib/kaboom/helpers/config.rb
166
+ - lib/kaboom/helpers/database_helper.rb
167
+ - lib/kaboom/helpers/utils.rb
168
+ - lib/kaboom/version.rb
169
+ homepage:
170
+ licenses:
171
+ - MIT
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: 2.7.0
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.5.11
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: A CLI for managing Rails apps deployed by Kamal.
192
+ test_files: []