capistrano-ops 1.0.11 → 1.2.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 +4 -4
- data/.github/dependabot.yml +18 -0
- data/.github/workflows/ci.yml +44 -0
- data/.github/workflows/release.yml +1 -1
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.rubocop.yml +4 -5
- data/.rubocop_todo.yml +55 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +48 -1
- data/README.md +101 -5
- data/Rakefile +5 -1
- data/bin/console +1 -1
- data/capistrano-ops.gemspec +10 -8
- data/lib/capistrano/ops/backup/helper.rb +4 -4
- data/lib/capistrano/ops/backup/tasks/database/create.rake +1 -0
- data/lib/capistrano/ops/backup/tasks/database/pull.rake +1 -0
- data/lib/capistrano/ops/backup/tasks/storage/create.rake +1 -0
- data/lib/capistrano/ops/backup/tasks/storage/pull.rake +1 -0
- data/lib/capistrano/ops/capistrano.rb +1 -0
- data/lib/capistrano/ops/figaro_yml/helpers.rb +3 -6
- data/lib/capistrano/ops/figaro_yml/tasks/check.rake +1 -0
- data/lib/capistrano/ops/figaro_yml/tasks/check_config_present.rake +1 -0
- data/lib/capistrano/ops/figaro_yml/tasks/check_figaro_file_exists.rake +1 -0
- data/lib/capistrano/ops/figaro_yml/tasks/check_git_tracking.rake +1 -0
- data/lib/capistrano/ops/figaro_yml/tasks/create_local.rake +1 -0
- data/lib/capistrano/ops/figaro_yml/tasks/figaro_yml_symlink.rake +1 -0
- data/lib/capistrano/ops/figaro_yml/tasks/get.rake +4 -3
- data/lib/capistrano/ops/figaro_yml/tasks/get_stage.rake +1 -0
- data/lib/capistrano/ops/figaro_yml/tasks/sort_local.rake +1 -0
- data/lib/capistrano/ops/figaro_yml.rb +2 -1
- data/lib/capistrano/ops/invoke/tasks/invoke.rake +1 -1
- data/lib/capistrano/ops/local/helpers.rb +71 -0
- data/lib/capistrano/ops/local/tasks/console.rake +59 -0
- data/lib/capistrano/ops/local/tasks/run.rake +40 -0
- data/lib/capistrano/ops/local.rb +9 -0
- data/lib/capistrano/ops/logrotate/paths.rb +2 -0
- data/lib/capistrano/ops/logrotate/tasks/disable.rake +1 -0
- data/lib/capistrano/ops/logrotate.rb +2 -1
- data/lib/capistrano/ops/rails/lib/backup/api.rb +1 -1
- data/lib/capistrano/ops/rails/lib/backup/s3.rb +9 -8
- data/lib/capistrano/ops/rails/lib/backup.rb +2 -0
- data/lib/capistrano/ops/rails/lib/notification/api.rb +1 -1
- data/lib/capistrano/ops/rails/lib/notification/slack.rb +4 -4
- data/lib/capistrano/ops/rails/lib/notification/webhook.rb +4 -4
- data/lib/capistrano/ops/rails/lib/tasks/pg/dump.rake +3 -3
- data/lib/capistrano/ops/rails/lib/tasks/pg/postgres_helper.rb +3 -3
- data/lib/capistrano/ops/rails/lib/tasks/pg/remove_old_dumps.rake +1 -1
- data/lib/capistrano/ops/rails/lib/tasks/storage/backup.rake +1 -1
- data/lib/capistrano/ops/rails/lib/tasks/storage/remove_old_backups.rake +2 -2
- data/lib/capistrano/ops/rails/lib/tasks/storage/storage_helper.rb +5 -5
- data/lib/capistrano/ops/version.rb +1 -1
- data/lib/capistrano/ops/whenever.rb +2 -1
- data/lib/capistrano/ops/wkhtmltopdf/tasks/setup.rake +1 -0
- data/spec/capistrano/ops/local/helpers_spec.rb +121 -0
- data/spec/capistrano/ops/version_spec.rb +9 -0
- data/spec/spec_helper.rb +20 -0
- metadata +63 -27
- data/.travis.yml +0 -13
- data/lib/capistrano/ops/capistrano/v3/tasks/backup.rake +0 -25
|
@@ -6,7 +6,7 @@ namespace :invoke do
|
|
|
6
6
|
|
|
7
7
|
desc 'Execute a rake task on a remote server (cap invoke:rake TASK=db:migrate)'
|
|
8
8
|
task :rake do
|
|
9
|
-
task_name = ENV
|
|
9
|
+
task_name = ENV.fetch('TASK', nil)
|
|
10
10
|
unless task_name
|
|
11
11
|
puts "\n\nFailed! You need to specify the 'TASK' parameter!",
|
|
12
12
|
'Usage: cap <stage> invoke:rake TASK=your:task',
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Capistrano
|
|
6
|
+
module Ops
|
|
7
|
+
module Local
|
|
8
|
+
module Helpers
|
|
9
|
+
def scripts_dir
|
|
10
|
+
File.expand_path(fetch(:local_scripts_dir, 'scripts/local'), Dir.pwd)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def available_scripts
|
|
14
|
+
Dir.glob(File.join(scripts_dir, '*.rb')).map { |f| File.basename(f) }.sort
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def resolve_script(name)
|
|
18
|
+
return nil if name.nil? || name.empty?
|
|
19
|
+
|
|
20
|
+
candidates = [name, "#{name}.rb"].map { |n| File.join(scripts_dir, n) }
|
|
21
|
+
candidates.find { |p| File.exist?(p) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def print_available_scripts
|
|
25
|
+
scripts = available_scripts
|
|
26
|
+
if scripts.empty?
|
|
27
|
+
puts " (no scripts found in #{fetch(:local_scripts_dir, 'scripts/local')}/)"
|
|
28
|
+
else
|
|
29
|
+
scripts.each { |s| puts " • #{s}" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def abort_script_not_given(command)
|
|
34
|
+
puts "\n#{color('Usage:', :bold)} cap <stage> local:#{command} SCRIPT=<filename>"
|
|
35
|
+
puts "\n#{color('Available scripts:', :bold)}"
|
|
36
|
+
print_available_scripts
|
|
37
|
+
abort
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def abort_script_not_found(name)
|
|
41
|
+
puts "\n#{color("Script not found: #{name}", :red)}"
|
|
42
|
+
puts "\n#{color('Available scripts:', :bold)}"
|
|
43
|
+
print_available_scripts
|
|
44
|
+
abort
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def confirm_dangerous_stage!(action)
|
|
48
|
+
stage = fetch(:stage).to_s
|
|
49
|
+
dangerous = fetch(:local_dangerous_stages, %w[production])
|
|
50
|
+
return unless dangerous.include?(stage)
|
|
51
|
+
|
|
52
|
+
print "\n#{color("⚠ #{action} on #{stage.upcase}. Continue? [y/N] ", :yellow)}"
|
|
53
|
+
abort 'Aborted.' unless $stdin.gets.to_s.strip.casecmp('y').zero?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def remote_tmp_path(script_path)
|
|
57
|
+
"/tmp/cap_local_#{SecureRandom.hex(6)}_#{File.basename(script_path)}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def color(text, style)
|
|
61
|
+
return text unless $stdout.tty?
|
|
62
|
+
|
|
63
|
+
code = { bold: '1', red: '31', green: '32', yellow: '33', cyan: '36', dim: '2' }[style]
|
|
64
|
+
return text unless code
|
|
65
|
+
|
|
66
|
+
"\e[#{code}m#{text}\e[0m"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :local do
|
|
4
|
+
include Capistrano::Ops::Local::Helpers
|
|
5
|
+
|
|
6
|
+
desc 'Rails console with a local script pre-loaded: cap <stage> local:console SCRIPT=<filename>'
|
|
7
|
+
task :console do
|
|
8
|
+
script = ENV.fetch('SCRIPT', nil)
|
|
9
|
+
abort_script_not_given('console') unless script
|
|
10
|
+
|
|
11
|
+
script_path = resolve_script(script)
|
|
12
|
+
abort_script_not_found(script) unless script_path
|
|
13
|
+
|
|
14
|
+
stage = fetch(:stage).to_s
|
|
15
|
+
puts "\n#{color('Script:', :bold)} #{File.basename(script_path)}"
|
|
16
|
+
puts "#{color('Target:', :bold)} #{stage} (interactive console)"
|
|
17
|
+
|
|
18
|
+
confirm_dangerous_stage!('Opening console')
|
|
19
|
+
|
|
20
|
+
remote_tmp = remote_tmp_path(script_path)
|
|
21
|
+
remote_irbrc = "/tmp/cap_local_irbrc_#{SecureRandom.hex(6)}.rb"
|
|
22
|
+
|
|
23
|
+
on primary(fetch(:console_role, :app)) do
|
|
24
|
+
upload! script_path, remote_tmp
|
|
25
|
+
# Wrapper loads the script into the console's main context and deletes
|
|
26
|
+
# both temp files server-side when IRB exits. The at_exit puts often
|
|
27
|
+
# gets swallowed by Sentry/etc during shutdown; the local puts in the
|
|
28
|
+
# ensure block below is what the user actually sees.
|
|
29
|
+
wrapper = <<~RUBY
|
|
30
|
+
load '#{remote_tmp}'
|
|
31
|
+
at_exit do
|
|
32
|
+
begin; File.delete('#{remote_tmp}'); rescue StandardError; end
|
|
33
|
+
begin; File.delete('#{remote_irbrc}'); rescue StandardError; end
|
|
34
|
+
end
|
|
35
|
+
RUBY
|
|
36
|
+
upload! StringIO.new(wrapper), remote_irbrc
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "#{color("▶ Opening console with #{File.basename(script_path)} on #{stage}…", :cyan)}\n\n"
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
run_interactively primary(fetch(:console_role, :app)), shell: fetch(:console_shell, nil) do
|
|
43
|
+
within current_path do
|
|
44
|
+
as user: fetch(:console_user, nil) do
|
|
45
|
+
args = ['-e', fetch(:console_env, fetch(:rails_env, fetch(:stage)))]
|
|
46
|
+
args.push('--', '-r', remote_irbrc)
|
|
47
|
+
execute(:rails, :console, *args)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
ensure
|
|
52
|
+
# Local puts — no `on {}` block, since Capistrano's Configuration is not
|
|
53
|
+
# safely Marshal-dumpable after run_interactively (fails with
|
|
54
|
+
# "no _dump_data is defined for class Thread::Mutex"). Server-side
|
|
55
|
+
# cleanup is handled by the wrapper's at_exit hook.
|
|
56
|
+
puts color("\n\u{1F9F9} Console closed on #{stage} — server temp files cleaned up on exit.", :dim)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :local do
|
|
4
|
+
include Capistrano::Ops::Local::Helpers
|
|
5
|
+
|
|
6
|
+
desc 'Run a local script on remote via rails runner: cap <stage> local:run SCRIPT=<filename>'
|
|
7
|
+
task :run do
|
|
8
|
+
script = ENV.fetch('SCRIPT', nil)
|
|
9
|
+
abort_script_not_given('run') unless script
|
|
10
|
+
|
|
11
|
+
script_path = resolve_script(script)
|
|
12
|
+
abort_script_not_found(script) unless script_path
|
|
13
|
+
|
|
14
|
+
stage = fetch(:stage).to_s
|
|
15
|
+
puts "\n#{color('Script:', :bold)} #{File.basename(script_path)}"
|
|
16
|
+
puts "#{color('Target:', :bold)} #{stage}"
|
|
17
|
+
|
|
18
|
+
confirm_dangerous_stage!('Running script')
|
|
19
|
+
|
|
20
|
+
on roles(fetch(:console_role, :app)) do
|
|
21
|
+
remote_tmp = remote_tmp_path(script_path)
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
upload! script_path, remote_tmp
|
|
25
|
+
puts "#{color("▶ Running #{File.basename(script_path)} on #{stage}…", :cyan)}\n\n"
|
|
26
|
+
|
|
27
|
+
within current_path do
|
|
28
|
+
with rails_env: fetch(:console_env, fetch(:rails_env, fetch(:stage))) do
|
|
29
|
+
execute :bundle, :exec, :rails, 'runner', remote_tmp
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
ensure
|
|
33
|
+
execute :rm, '-f', remote_tmp
|
|
34
|
+
puts color("\u{1F9F9} Cleaned up temp file on server", :dim)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
puts "\n#{color('✔ Done.', :green)}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'capistrano/ops/task_loader'
|
|
4
|
+
require 'capistrano/ops/local/helpers'
|
|
5
|
+
|
|
6
|
+
TaskLoader.load_tasks_if_gem_present(
|
|
7
|
+
'capistrano-rails', 'local/tasks',
|
|
8
|
+
'WARNING: Gemfile does not include capistrano-rails gem which is required for local:console (provides capistrano/rails/console)'
|
|
9
|
+
)
|
|
@@ -4,4 +4,5 @@ require 'capistrano/ops/task_loader'
|
|
|
4
4
|
require 'capistrano/ops/logrotate/helpers'
|
|
5
5
|
require 'capistrano/ops/logrotate/paths'
|
|
6
6
|
|
|
7
|
-
TaskLoader.load_tasks_if_gem_present('whenever', 'logrotate/tasks',
|
|
7
|
+
TaskLoader.load_tasks_if_gem_present('whenever', 'logrotate/tasks',
|
|
8
|
+
'WARNING: Gemfile does not include whenever gem which is required for logrotate tasks')
|
|
@@ -4,7 +4,7 @@ module Backup
|
|
|
4
4
|
class Api
|
|
5
5
|
attr_accessor :backup_provider, :provider_config
|
|
6
6
|
|
|
7
|
-
def initialize(provider: ENV
|
|
7
|
+
def initialize(provider: ENV.fetch('BACKUP_PROVIDER', nil), provider_config: {})
|
|
8
8
|
self.backup_provider = provider
|
|
9
9
|
self.provider_config = provider_config
|
|
10
10
|
end
|
|
@@ -12,8 +12,9 @@ module Backup
|
|
|
12
12
|
|
|
13
13
|
attr_accessor :endpoint, :region, :access_key_id, :secret_access_key, :s3_resource, :s3_client
|
|
14
14
|
|
|
15
|
-
def initialize(endpoint: ENV
|
|
16
|
-
|
|
15
|
+
def initialize(endpoint: ENV.fetch('S3_BACKUP_ENDPOINT',
|
|
16
|
+
nil), region: ENV.fetch('S3_BACKUP_REGION', nil), access_key_id: ENV.fetch('S3_BACKUP_KEY', nil),
|
|
17
|
+
secret_access_key: ENV.fetch('S3_BACKUP_SECRET', nil))
|
|
17
18
|
self.endpoint = endpoint
|
|
18
19
|
self.region = region
|
|
19
20
|
self.access_key_id = access_key_id
|
|
@@ -31,7 +32,7 @@ module Backup
|
|
|
31
32
|
|
|
32
33
|
def upload(backup_file, key)
|
|
33
34
|
begin
|
|
34
|
-
s3_resource.bucket(ENV
|
|
35
|
+
s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).object(key).upload_file(backup_file)
|
|
35
36
|
rescue Backup::Error => e
|
|
36
37
|
puts "Upload failed: #{e.message}"
|
|
37
38
|
raise e
|
|
@@ -49,7 +50,7 @@ module Backup
|
|
|
49
50
|
|
|
50
51
|
# rubocop:disable Metrics/MethodLength
|
|
51
52
|
def upload_file_as_stream(file_path, key)
|
|
52
|
-
bucket = ENV
|
|
53
|
+
bucket = ENV.fetch('S3_BACKUP_BUCKET', nil)
|
|
53
54
|
# Calculate total size of the file to be uploaded
|
|
54
55
|
total_size = File.size(file_path)
|
|
55
56
|
chunk_size = calculate_chunk_size(total_size)
|
|
@@ -130,7 +131,7 @@ module Backup
|
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
def upload_folder_as_tar_gz_stream(folder_path, key)
|
|
133
|
-
bucket = ENV
|
|
134
|
+
bucket = ENV.fetch('S3_BACKUP_BUCKET', nil)
|
|
134
135
|
|
|
135
136
|
# Calculate total size of the files to be uploaded
|
|
136
137
|
total_size = calculate_total_size(folder_path)
|
|
@@ -256,7 +257,7 @@ module Backup
|
|
|
256
257
|
end
|
|
257
258
|
|
|
258
259
|
def remove_old_backups(basename, keep: 5)
|
|
259
|
-
all_items = s3_resource.bucket(ENV
|
|
260
|
+
all_items = s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).objects(prefix: basename).map do |item|
|
|
260
261
|
{ key: item.key, last_modified: item.last_modified }
|
|
261
262
|
end
|
|
262
263
|
|
|
@@ -272,7 +273,7 @@ module Backup
|
|
|
272
273
|
puts "Removing #{month} from S3"
|
|
273
274
|
delete_items.each do |item_obj|
|
|
274
275
|
puts "Removing #{item_obj[:key]} from S3"
|
|
275
|
-
s3_resource.bucket(ENV
|
|
276
|
+
s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).object(item_obj[:key]).delete
|
|
276
277
|
end
|
|
277
278
|
end
|
|
278
279
|
puts 'Old months removed from S3'
|
|
@@ -286,7 +287,7 @@ module Backup
|
|
|
286
287
|
|
|
287
288
|
current_month_delete_items.each do |item_obj|
|
|
288
289
|
puts "Removing #{item_obj[:key]} from S3"
|
|
289
|
-
s3_resource.bucket(ENV
|
|
290
|
+
s3_resource.bucket(ENV.fetch('S3_BACKUP_BUCKET', nil)).object(item_obj[:key]).delete
|
|
290
291
|
end
|
|
291
292
|
puts 'Old backups removed from S3'
|
|
292
293
|
rescue Backup::Error => e
|
|
@@ -4,7 +4,7 @@ module Notification
|
|
|
4
4
|
class Api
|
|
5
5
|
attr_accessor :notification_type, :notification_level
|
|
6
6
|
|
|
7
|
-
def initialize(notification_type: ENV
|
|
7
|
+
def initialize(notification_type: ENV.fetch('NOTIFICATION_TYPE', nil), notification_level: ENV.fetch('NOTIFICATION_LEVEL', nil))
|
|
8
8
|
self.notification_type = notification_type
|
|
9
9
|
self.notification_level = notification_level || 'error'
|
|
10
10
|
end
|
|
@@ -6,8 +6,8 @@ require 'json'
|
|
|
6
6
|
module Notification
|
|
7
7
|
class Slack
|
|
8
8
|
def initialize
|
|
9
|
-
@slack_secret = ENV
|
|
10
|
-
@slack_channel = ENV
|
|
9
|
+
@slack_secret = ENV.fetch('SLACK_SECRET', nil)
|
|
10
|
+
@slack_channel = ENV.fetch('SLACK_CHANNEL', nil)
|
|
11
11
|
@conn = Faraday.new(url: 'https://slack.com/api/') do |faraday|
|
|
12
12
|
faraday.headers['Content-Type'] = 'application/json'
|
|
13
13
|
faraday.headers['Authorization'] = "Bearer #{@slack_secret}"
|
|
@@ -20,8 +20,8 @@ module Notification
|
|
|
20
20
|
begin
|
|
21
21
|
res = @conn.post('chat.postMessage') do |req|
|
|
22
22
|
req.body = {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
channel: @slack_channel,
|
|
24
|
+
text: message
|
|
25
25
|
}.to_json
|
|
26
26
|
end
|
|
27
27
|
response = JSON.parse(res.body)
|
|
@@ -7,18 +7,18 @@ module Notification
|
|
|
7
7
|
require 'json'
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
|
-
@webhook_url = ENV
|
|
11
|
-
@secret = ENV
|
|
10
|
+
@webhook_url = ENV.fetch('WEBHOOK_URL', nil)
|
|
11
|
+
@secret = ENV.fetch('WEBHOOK_SECRET', nil)
|
|
12
12
|
@conn = Faraday.new(url: @webhook_url) do |faraday|
|
|
13
13
|
faraday.headers['Content-Type'] = 'application/json'
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def generate_signature(payload_body)
|
|
18
|
-
"md5=#{OpenSSL::HMAC.hexdigest('md5', ENV
|
|
18
|
+
"md5=#{OpenSSL::HMAC.hexdigest('md5', ENV.fetch('WEBHOOK_SECRET', nil), payload_body)}"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def backup_notification(result, webhook_data,
|
|
21
|
+
def backup_notification(result, webhook_data, notification_level)
|
|
22
22
|
return if @webhook_url.nil? || @secret.nil?
|
|
23
23
|
return if result && notification_level == 'error'
|
|
24
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'postgres_helper'
|
|
4
4
|
namespace :pg do
|
|
5
5
|
include PostgresHelper
|
|
6
6
|
|
|
@@ -25,11 +25,11 @@ namespace :pg do
|
|
|
25
25
|
result = system(commandlist)
|
|
26
26
|
|
|
27
27
|
if ENV['BACKUP_PROVIDER'].present? && external_backup && result
|
|
28
|
-
puts "Uploading #{filename} to #{ENV
|
|
28
|
+
puts "Uploading #{filename} to #{ENV.fetch('BACKUP_PROVIDER', nil)}..."
|
|
29
29
|
provider = Backup::Api.new
|
|
30
30
|
begin
|
|
31
31
|
provider.upload("#{backup_path}/#{filename}", filename.to_s, 'file')
|
|
32
|
-
puts "#{filename} uploaded to #{ENV
|
|
32
|
+
puts "#{filename} uploaded to #{ENV.fetch('BACKUP_PROVIDER', nil)}"
|
|
33
33
|
rescue StandardError => e
|
|
34
34
|
puts "#{filename} upload failed: #{e.message}"
|
|
35
35
|
end
|
|
@@ -27,7 +27,7 @@ module PostgresHelper
|
|
|
27
27
|
messages = []
|
|
28
28
|
if result
|
|
29
29
|
messages << "Backup of #{database} successfully finished at #{Time.now}"
|
|
30
|
-
messages << "Backup path
|
|
30
|
+
messages << "Backup path:`#{backup_path}/#{filename}`"
|
|
31
31
|
else
|
|
32
32
|
messages << "Backup of #{database} failed at #{Time.now}"
|
|
33
33
|
end
|
|
@@ -54,14 +54,14 @@ module PostgresHelper
|
|
|
54
54
|
commandlist = []
|
|
55
55
|
commandlist << "export PGPASSWORD='#{password}'"
|
|
56
56
|
commandlist << "cd #{backup_path}"
|
|
57
|
-
commandlist << "pg_dump --no-acl --no-owner #{options.join
|
|
57
|
+
commandlist << "pg_dump --no-acl --no-owner #{options.join} > #{filename}"
|
|
58
58
|
commandlist.join(' && ')
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def size_str(size)
|
|
62
62
|
units = %w[B KB MB GB TB]
|
|
63
63
|
e = (Math.log(size) / Math.log(1024)).floor
|
|
64
|
-
s = format('%.2f', size.to_f / 1024**e)
|
|
64
|
+
s = format('%.2f', size.to_f / (1024**e))
|
|
65
65
|
s.sub(/\.?0*$/, units[e])
|
|
66
66
|
end
|
|
67
67
|
end
|
|
@@ -12,7 +12,7 @@ namespace :pg do
|
|
|
12
12
|
external_backup = Rails.env.production? || ENV['EXTERNAL_BACKUP_ENABLED'] == 'true'
|
|
13
13
|
|
|
14
14
|
task :remove_old_dumps do
|
|
15
|
-
bash_regex = "'#{@database}.{0,}
|
|
15
|
+
bash_regex = "'#{@database}.{0,}.dump'"
|
|
16
16
|
|
|
17
17
|
unless backups_enabled
|
|
18
18
|
puts 'remove_old_dumps: Backups are disabled'
|
|
@@ -14,7 +14,7 @@ namespace :storage do
|
|
|
14
14
|
@total_external_backups_no = (@env_external_no || ENV['NUMBER_OF_BACKUPS'] || 7).to_i
|
|
15
15
|
desc 'remove old storage backups'
|
|
16
16
|
task :remove_old_backups do
|
|
17
|
-
bash_regex = "'storage_.{0,}
|
|
17
|
+
bash_regex = "'storage_.{0,}.tar.gz'"
|
|
18
18
|
|
|
19
19
|
unless backups_enabled
|
|
20
20
|
puts 'remove_old_backups: Backups are disabled'
|
|
@@ -37,7 +37,7 @@ namespace :storage do
|
|
|
37
37
|
'xargs rm -rf'
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
result = system(commandlist.join(' | ')) if @total_local_backups_no.positive? && local_backup || !local_backup && external_backup
|
|
40
|
+
result = system(commandlist.join(' | ')) if (@total_local_backups_no.positive? && local_backup) || (!local_backup && external_backup)
|
|
41
41
|
puts 'remove_old_backups: local cleanup finished' if result
|
|
42
42
|
|
|
43
43
|
if ENV['BACKUP_PROVIDER'].present? && external_backup
|
|
@@ -9,7 +9,7 @@ module StorageHelper
|
|
|
9
9
|
backups_enabled: env_or_production('BACKUPS_ENABLED'),
|
|
10
10
|
external_backup: env_or_production('EXTERNAL_BACKUP_ENABLED'),
|
|
11
11
|
keep_local_backups: env_or_production('KEEP_LOCAL_STORAGE_BACKUPS'),
|
|
12
|
-
backup_provider: ENV
|
|
12
|
+
backup_provider: ENV.fetch('BACKUP_PROVIDER', nil)
|
|
13
13
|
}
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -23,7 +23,7 @@ module StorageHelper
|
|
|
23
23
|
messages = []
|
|
24
24
|
if result
|
|
25
25
|
messages << "Backup of storage folder successfully finished at #{Time.now}"
|
|
26
|
-
messages << "Backup path
|
|
26
|
+
messages << "Backup path:`#{@backup_path}/#{@filename}`"
|
|
27
27
|
else
|
|
28
28
|
messages << "Backup of storage folder failed at #{Time.now}"
|
|
29
29
|
end
|
|
@@ -34,19 +34,19 @@ module StorageHelper
|
|
|
34
34
|
@backup_path = settings[:backup_path]
|
|
35
35
|
@date = Time.now.to_i
|
|
36
36
|
@filename = "storage_#{@date}.tar.gz"
|
|
37
|
-
FileUtils.mkdir_p(@backup_path)
|
|
37
|
+
FileUtils.mkdir_p(@backup_path)
|
|
38
38
|
"tar -zcf #{@backup_path}/#{@filename} -C #{settings[:storage_path]} ."
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def size_str(size)
|
|
42
42
|
units = %w[B KB MB GB TB]
|
|
43
43
|
e = (Math.log(size) / Math.log(1024)).floor
|
|
44
|
-
s = format('%.2f', size.to_f / 1024**e)
|
|
44
|
+
s = format('%.2f', size.to_f / (1024**e))
|
|
45
45
|
s.sub(/\.?0*$/, units[e])
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def create_local_backup(filename, storage_path, backup_path)
|
|
49
|
-
FileUtils.mkdir_p(backup_path)
|
|
49
|
+
FileUtils.mkdir_p(backup_path)
|
|
50
50
|
response = system(backup_cmd(backup_path: backup_path, storage_path: storage_path))
|
|
51
51
|
FileUtils.rm_rf("#{@backup_path}/#{filename}") unless response
|
|
52
52
|
puts response ? "Backup created: #{backup_path}/#{@filename} (#{size_str(File.size("#{@backup_path}/#{@filename}"))})" : 'Backup failed removing dump file'
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
require 'capistrano/ops/task_loader'
|
|
4
4
|
|
|
5
|
-
TaskLoader.load_tasks_if_gem_present('whenever', 'whenever/tasks',
|
|
5
|
+
TaskLoader.load_tasks_if_gem_present('whenever', 'whenever/tasks',
|
|
6
|
+
'WARNING: Gemfile does not include whenever gem which is required for whenever tasks')
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'capistrano/ops/local/helpers'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Capistrano::Ops::Local::Helpers do
|
|
8
|
+
let(:host) do
|
|
9
|
+
Class.new do
|
|
10
|
+
include Capistrano::Ops::Local::Helpers
|
|
11
|
+
|
|
12
|
+
def initialize(scripts_dir_override)
|
|
13
|
+
@scripts_dir_override = scripts_dir_override
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch(key, default = nil)
|
|
17
|
+
return @scripts_dir_override if key == :local_scripts_dir
|
|
18
|
+
|
|
19
|
+
default
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
around do |example|
|
|
25
|
+
Dir.mktmpdir do |dir|
|
|
26
|
+
@tmp = dir
|
|
27
|
+
Dir.chdir(dir) { example.run }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write_script(name)
|
|
32
|
+
dir = File.join(@tmp, 'ops', 'scripts')
|
|
33
|
+
FileUtils.mkdir_p(dir)
|
|
34
|
+
File.write(File.join(dir, name), '# example')
|
|
35
|
+
dir
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#available_scripts' do
|
|
39
|
+
it 'returns sorted basenames of .rb files' do
|
|
40
|
+
write_script('b.rb')
|
|
41
|
+
write_script('a.rb')
|
|
42
|
+
write_script('note.txt')
|
|
43
|
+
|
|
44
|
+
instance = host.new('ops/scripts')
|
|
45
|
+
|
|
46
|
+
expect(instance.available_scripts).to eq(%w[a.rb b.rb])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'is empty when the directory does not exist' do
|
|
50
|
+
instance = host.new('does/not/exist')
|
|
51
|
+
|
|
52
|
+
expect(instance.available_scripts).to eq([])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#resolve_script' do
|
|
57
|
+
it 'returns the full path when the exact filename exists' do
|
|
58
|
+
write_script('foo.rb')
|
|
59
|
+
|
|
60
|
+
instance = host.new('ops/scripts')
|
|
61
|
+
|
|
62
|
+
expect(instance.resolve_script('foo.rb')).to end_with('/ops/scripts/foo.rb')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'auto-appends .rb when omitted' do
|
|
66
|
+
write_script('foo.rb')
|
|
67
|
+
|
|
68
|
+
instance = host.new('ops/scripts')
|
|
69
|
+
|
|
70
|
+
expect(instance.resolve_script('foo')).to end_with('/ops/scripts/foo.rb')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns nil for unknown names' do
|
|
74
|
+
write_script('foo.rb')
|
|
75
|
+
|
|
76
|
+
instance = host.new('ops/scripts')
|
|
77
|
+
|
|
78
|
+
expect(instance.resolve_script('bar')).to be_nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'returns nil for empty input' do
|
|
82
|
+
instance = host.new('ops/scripts')
|
|
83
|
+
|
|
84
|
+
expect(instance.resolve_script(nil)).to be_nil
|
|
85
|
+
expect(instance.resolve_script('')).to be_nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#remote_tmp_path' do
|
|
90
|
+
it 'produces a unique /tmp path preserving the basename' do
|
|
91
|
+
instance = host.new('ops/scripts')
|
|
92
|
+
|
|
93
|
+
path = instance.remote_tmp_path('/some/where/foo.rb')
|
|
94
|
+
|
|
95
|
+
expect(path).to match(%r{\A/tmp/cap_local_[0-9a-f]{12}_foo\.rb\z})
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '#color' do
|
|
100
|
+
it 'returns plain text when stdout is not a TTY' do
|
|
101
|
+
instance = host.new('ops/scripts')
|
|
102
|
+
allow($stdout).to receive(:tty?).and_return(false)
|
|
103
|
+
|
|
104
|
+
expect(instance.color('hello', :red)).to eq('hello')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'wraps text in ANSI escape when stdout is a TTY and style is known' do
|
|
108
|
+
instance = host.new('ops/scripts')
|
|
109
|
+
allow($stdout).to receive(:tty?).and_return(true)
|
|
110
|
+
|
|
111
|
+
expect(instance.color('hello', :red)).to eq("\e[31mhello\e[0m")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'passes text through unchanged for unknown styles' do
|
|
115
|
+
instance = host.new('ops/scripts')
|
|
116
|
+
allow($stdout).to receive(:tty?).and_return(true)
|
|
117
|
+
|
|
118
|
+
expect(instance.color('hello', :magenta)).to eq('hello')
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'capistrano/ops/version'
|
|
4
|
+
|
|
5
|
+
RSpec.configure do |config|
|
|
6
|
+
config.expect_with :rspec do |expectations|
|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
config.mock_with :rspec do |mocks|
|
|
11
|
+
mocks.verify_partial_doubles = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
|
15
|
+
config.disable_monkey_patching!
|
|
16
|
+
config.warnings = false
|
|
17
|
+
config.default_formatter = 'doc' if config.files_to_run.one?
|
|
18
|
+
config.order = :random
|
|
19
|
+
Kernel.srand config.seed
|
|
20
|
+
end
|