capistrano-ops 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +13 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/capistrano-ops.gemspec +24 -0
- data/lib/capistrano/ops/capistrano/v3/tasks/backup.rake +29 -0
- data/lib/capistrano/ops/capistrano/v3/tasks/figaro_yml.rake +80 -0
- data/lib/capistrano/ops/capistrano/v3/tasks/invoke.rake +24 -0
- data/lib/capistrano/ops/capistrano/v3/tasks/logs.rake +16 -0
- data/lib/capistrano/ops/capistrano/v3/tasks/whenever.rake +12 -0
- data/lib/capistrano/ops/capistrano.rb +13 -0
- data/lib/capistrano/ops/notification/api.rb +29 -0
- data/lib/capistrano/ops/notification/slack.rb +66 -0
- data/lib/capistrano/ops/notification/webhook.rb +40 -0
- data/lib/capistrano/ops/notification.rb +7 -0
- data/lib/capistrano/ops/railtie.rb +15 -0
- data/lib/capistrano/ops/tasks/pg/dump.rake +39 -0
- data/lib/capistrano/ops/tasks/pg/remove_old_dumps.rake +19 -0
- data/lib/capistrano/ops/version.rb +6 -0
- data/lib/capistrano/ops.rb +24 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 772b42fc59038f214889f2e20c4d3978d932d2212a5d8c231622333570b37daa
|
4
|
+
data.tar.gz: bf996f05934e11484667fe0a8e4c0b4027acd4cfefa7f4d3e54541a71a60ce5b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3fddcba0cc8ce3b3a6bb363dadd9e76261478820dd21247f1f96ed03dfd037f3a93b9c14e572d7f406b0721892417ac44fc36a0e7f6922d56eaa3f6ac0b0160b
|
7
|
+
data.tar.gz: '09bbf508cae370818e13a8753d07991cb954f0749db120150200ef2a738e5fee347235de19d2a1cf7b67008e86422aa89bf0b84dd6a6499ccf517bbb834ff069'
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 zauberware
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# capistrano-ops
|
2
|
+
|
3
|
+
Library of useful scripts for DevOps using capistrano with rails.
|
4
|
+
|
5
|
+
**Only supports Capistrano 3 and above**.
|
6
|
+
|
7
|
+
## Requirements
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
'capistrano', '~> 3.0'
|
11
|
+
'whenever'
|
12
|
+
```
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add the gem to your `Gemfile` after setting up Capistrano
|
17
|
+
group:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
group :development do
|
21
|
+
gem 'capistrano', require: false
|
22
|
+
end
|
23
|
+
|
24
|
+
gem 'capistrano-ops'
|
25
|
+
```
|
26
|
+
|
27
|
+
Then `bundle` and add it to your `Capfile`
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# Capfile
|
31
|
+
|
32
|
+
require 'capistrano/ops'
|
33
|
+
```
|
34
|
+
|
35
|
+
and `initializers`
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# initializers/capistrano-ops.rb
|
39
|
+
require 'capistrano/ops'
|
40
|
+
```
|
41
|
+
|
42
|
+
## Script overview
|
43
|
+
|
44
|
+
| Script | Description |
|
45
|
+
| ------------------------------------------------ | ----------------------------------------------------------------- |
|
46
|
+
| `cap <environment> backup:create` | creates backup of postgres database on the server |
|
47
|
+
| `cap <environment> backup:pull` | download latest postgres backup from server |
|
48
|
+
| `cap <environment> figaro_yml:compare` | compare local application.yml with server application.yml |
|
49
|
+
| `cap <environment> figaro_yml:get` | shows env vars from server application.yml configured thru figaro |
|
50
|
+
| `cap <environment> logs:rails` | display server log live |
|
51
|
+
| `cap <environment> whenever:show_crontab` | display server app crontab generated with whenever |
|
52
|
+
| `cap <environment> invoke:rake TASK=<your:task>` | invoke rake task on server |
|
53
|
+
| `rake pg:dump` | creates postgres database backup |
|
54
|
+
| `rake pg:remove_old_dumps` | remove old postgres backups |
|
55
|
+
|
56
|
+
## Usage
|
57
|
+
|
58
|
+
for all backup task you have to setup your database.yml properly:
|
59
|
+
|
60
|
+
```
|
61
|
+
production:
|
62
|
+
|
63
|
+
database: database_name
|
64
|
+
username: database_username
|
65
|
+
password: database_password
|
66
|
+
host: database_host
|
67
|
+
port: database_port
|
68
|
+
```
|
69
|
+
|
70
|
+
### Optional Settings for backup task
|
71
|
+
|
72
|
+
| env | description | type/options |
|
73
|
+
| ----------------- | ---------------------------------------------------------------------- | :----------------------------------------------------------------: |
|
74
|
+
| NUMBER_OF_BACKUPS | number of backups to keep (default: 1) | `number` |
|
75
|
+
| BACKUPS_ENABLED | enable/disable backup task (default: Rails.env == 'production') | `boolean` |
|
76
|
+
| DEFAULT_URL | notification message title (default: "#{database} Backup") | `string` |
|
77
|
+
| NOTIFICATION_TYPE | for notification (default: nil) | `string` (`webhook`/`slack`) |
|
78
|
+
| SLACK_SECRET | for slack integration | `string` (e.g. `xoxb-1234567890-1234567890-1234567890-1234567890`) |
|
79
|
+
| SLACK_CHANNEL | for slack integration | `string` (e.g. `C234567890`) |
|
80
|
+
| WEBHOOK_URL | Webhook server to send message | e.g `http://example.com` |
|
81
|
+
| WEBHOOK_SECRET | Secret to send with uses md5-hmac hexdigest in header`x-hub-signature` | --- |
|
82
|
+
|
83
|
+
### use with whenever/capistrano
|
84
|
+
|
85
|
+
install whenever gem and add this to your schedule.rb
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# config/schedule.rb
|
89
|
+
# Use this file to easily define all of your cron jobs.
|
90
|
+
env :PATH, ENV['PATH']
|
91
|
+
set :output, -> { '2>&1 | logger -t whenever_cron' }
|
92
|
+
|
93
|
+
every :day, at: '2:00 am' do
|
94
|
+
rake 'pg:dump'
|
95
|
+
end
|
96
|
+
|
97
|
+
every :day, at: '3:00 am' do
|
98
|
+
rake 'pg:remove_old_dumps'
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
add this to your capfile
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# Capfile
|
106
|
+
require 'whenever/capistrano'
|
107
|
+
```
|
108
|
+
|
109
|
+
## Configuration
|
110
|
+
|
111
|
+
You can optionally specify the capistrano roles for the rake task (Defaults to `:app`):
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# Defaults to [:app]
|
115
|
+
set :rake_roles, %i[db app]
|
116
|
+
```
|
117
|
+
|
118
|
+
## Slack integration
|
119
|
+
|
120
|
+
if you want to use slack integration you have to add this to your `application.yml`
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
NOTIFICATION_TYPE: 'slack'
|
124
|
+
SLACK_SECRET: '<your-slack-secret>'
|
125
|
+
SLACK_CHANNEL: '<your-slack-channel>'
|
126
|
+
```
|
127
|
+
|
128
|
+
## Webhook integration
|
129
|
+
|
130
|
+
if you want to use webhook integration you have to add this to your `application.yml`
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
NOTIFICATION_TYPE: 'webhook'
|
134
|
+
WEBHOOK_URL: '<your-webhook-url>'
|
135
|
+
WEBHOOK_SECRET: '<your-webhook-secret>'
|
136
|
+
```
|
137
|
+
|
138
|
+
## Contributing
|
139
|
+
|
140
|
+
1. Fork it ( https://github.com/zauberware/capistrano-ops/fork )
|
141
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
142
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
143
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
144
|
+
5. Create a new Pull Request
|
145
|
+
|
146
|
+
## License
|
147
|
+
|
148
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
149
|
+
|
150
|
+
```
|
151
|
+
|
152
|
+
```
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'capistrano/rake'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
|
5
|
+
require 'capistrano/ops/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = 'capistrano-ops'
|
9
|
+
s.version = Capistrano::Ops::VERSION
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.authors = ['Florian Crusius']
|
12
|
+
s.email = ['florian@zauberware.com']
|
13
|
+
s.license = 'MIT'
|
14
|
+
s.homepage = 'https://github.com/zauberware/capistrano-ops'
|
15
|
+
s.summary = 'devops tasks for rails applications'
|
16
|
+
s.description = 'A collection of devops tasks for rails applications'
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- test/{functional,unit}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
20
|
+
s.require_paths = ['lib']
|
21
|
+
|
22
|
+
s.add_development_dependency 'bundler', '~> 2.3.9'
|
23
|
+
s.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :backup do
|
4
|
+
# Default to :app role
|
5
|
+
rake_roles = fetch(:rake_roles, :app)
|
6
|
+
|
7
|
+
desc 'create a backup of the server database'
|
8
|
+
task :create do
|
9
|
+
on roles(rake_roles) do
|
10
|
+
env = "RAILS_ENV=#{fetch(:stage)}"
|
11
|
+
# rubocop:disable Layout/LineLength
|
12
|
+
path_cmd = "PATH=$HOME/.rbenv/versions/#{RUBY_VERSION}/bin:$PATH"
|
13
|
+
# rubocop:enable Layout/LineLength
|
14
|
+
execute "cd #{release_path} && #{path_cmd} && #{env} BACKUPS_ENABLED=true bundle exec rake pg:dump"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
desc 'pull latest database backups from server to local'
|
18
|
+
task :pull do
|
19
|
+
on roles(rake_roles) do
|
20
|
+
# rubocop:disable Layout/LineLength
|
21
|
+
execute "cd #{shared_path}/backups && tar -czf #{shared_path}/backups.tar.gz $(ls -lt | grep -E -i '.{0,}\.dump' | head -n 1 | awk '{print $9}')"
|
22
|
+
# rubocop:enable Layout/LineLength
|
23
|
+
download! "#{shared_path}/backups.tar.gz", 'backups.tar.gz'
|
24
|
+
execute "rm #{shared_path}/backups.tar.gz"
|
25
|
+
system 'tar -xzf backups.tar.gz'
|
26
|
+
system 'rm backups.tar.gz'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
namespace :figaro_yml do
|
5
|
+
# Defaults to :app role
|
6
|
+
rake_roles = fetch(:rake_roles, :app)
|
7
|
+
|
8
|
+
desc 'get the figaro_yml file from the server'
|
9
|
+
task :get do
|
10
|
+
on roles(rake_roles) do
|
11
|
+
|
12
|
+
puts capture "cat #{shared_path}/config/application.yml"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'compare and set the figaro_yml file on the server'
|
17
|
+
task :compare do
|
18
|
+
env = fetch(:stage)
|
19
|
+
# read local application.yml
|
20
|
+
local = File.read('config/application.yml')
|
21
|
+
|
22
|
+
# convert to hash
|
23
|
+
local_global_env = YAML.safe_load(local)
|
24
|
+
|
25
|
+
# split into stage and global
|
26
|
+
local_stage_env = local_global_env[env.to_s]
|
27
|
+
local_global_env.delete('staging')
|
28
|
+
local_global_env.delete('production')
|
29
|
+
|
30
|
+
on roles(rake_roles) do
|
31
|
+
# read remote application.yml
|
32
|
+
remote = capture("cat #{shared_path}/config/application.yml")
|
33
|
+
|
34
|
+
remote_global_env = YAML.safe_load(remote)
|
35
|
+
remote_stage_env = remote_global_env[env.to_s]
|
36
|
+
remote_global_env.delete(env.to_s)
|
37
|
+
|
38
|
+
puts "with command 'cap #{env} figaro_yml:setup', following variables will be overwritten:"
|
39
|
+
puts '--------------------------------------------------------------------------------'
|
40
|
+
result1 = compare_hashes(local_global_env, remote_global_env)
|
41
|
+
result2 = compare_hashes(local_stage_env, remote_stage_env)
|
42
|
+
if !result1.empty? || !result2.empty?
|
43
|
+
loop do
|
44
|
+
print 'Update remote application.yml? (y/N): '
|
45
|
+
input = $stdin.gets.strip.downcase
|
46
|
+
answer = (input.empty? ? 'N' : input).downcase.to_s
|
47
|
+
|
48
|
+
next unless %w(y n).include?(answer)
|
49
|
+
|
50
|
+
if answer == 'y'
|
51
|
+
puts 'Updating remote application.yml'
|
52
|
+
invoke 'figaro_yml:setup'
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
break
|
56
|
+
end
|
57
|
+
puts 'remote application.yml not updated'
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
puts 'remote application.yml is up to date'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
def compare_hashes(hash1, hash2)
|
64
|
+
changes = false
|
65
|
+
local_server = hash1.to_a - hash2.to_a
|
66
|
+
server_local = hash2.to_a - hash1.to_a
|
67
|
+
|
68
|
+
[local_server + server_local].flatten(1).to_h.keys.each do |k|
|
69
|
+
new_value = hash1[k].to_s
|
70
|
+
new_value = new_value.empty? ? "nil" : new_value
|
71
|
+
old_value = hash2[k].to_s
|
72
|
+
old_value = old_value.empty? ? "nil" : old_value
|
73
|
+
if old_value != new_value
|
74
|
+
puts "#{k}: #{old_value} => #{new_value} \r\n"
|
75
|
+
changes = true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,24 @@
|
|
1
|
+
namespace :invoke do
|
2
|
+
|
3
|
+
# Defalut to :app roles
|
4
|
+
rake_roles = fetch(:rake_roles, :app)
|
5
|
+
|
6
|
+
desc "Execute a rake task on a remote server (cap invoke:rake TASK=db:migrate)"
|
7
|
+
task :rake do
|
8
|
+
if ENV['TASK']
|
9
|
+
on roles(rake_roles) do
|
10
|
+
within current_path do
|
11
|
+
with rails_env: fetch(:rails_env) do
|
12
|
+
execute :rake, ENV['TASK']
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
else
|
18
|
+
puts "\n\nFailed! You need to specify the 'TASK' parameter!",
|
19
|
+
"Usage: cap <stage> invoke:rake TASK=your:task"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :logs do
|
4
|
+
# Default to :app role
|
5
|
+
rake_roles = fetch(:rake_roles, :app)
|
6
|
+
desc 'tail rails logs'
|
7
|
+
task :rails do
|
8
|
+
on roles(rake_roles) do
|
9
|
+
trap('SIGINT') do
|
10
|
+
puts "\nDisconnecting..."
|
11
|
+
exit
|
12
|
+
end
|
13
|
+
execute "tail -f #{shared_path}/log/#{fetch(:rails_env)}.log"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capistrano/version'
|
4
|
+
|
5
|
+
if defined?(Capistrano::VERSION) && Gem::Version.new(Capistrano::VERSION).release >= Gem::Version.new('3.0.0')
|
6
|
+
load File.expand_path('capistrano/v3/tasks/whenever.rake', __dir__)
|
7
|
+
load File.expand_path('capistrano/v3/tasks/backup.rake', __dir__)
|
8
|
+
load File.expand_path('capistrano/v3/tasks/figaro_yml.rake', __dir__)
|
9
|
+
load File.expand_path('capistrano/v3/tasks/logs.rake', __dir__)
|
10
|
+
load File.expand_path('capistrano/v3/tasks/invoke.rake', __dir__)
|
11
|
+
else
|
12
|
+
puts 'Capistrano 3 is required to use this gem'
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Notification
|
2
|
+
class Api
|
3
|
+
attr_accessor :notification_type
|
4
|
+
|
5
|
+
def initialize(notification_type: ENV['NOTIFICATION_TYPE'])
|
6
|
+
self.notification_type = notification_type
|
7
|
+
end
|
8
|
+
|
9
|
+
def send_backup_notification(result, date, database, backup_path)
|
10
|
+
return if notification_type.nil?
|
11
|
+
case notification_type
|
12
|
+
when 'slack'
|
13
|
+
Slack.new.backup_notification(result, date, database, backup_path)
|
14
|
+
when 'webhook'
|
15
|
+
Webhook.new.backup_notification(result, date, database, backup_path)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_notification(message)
|
20
|
+
return if notification_type.nil?
|
21
|
+
case notification_type
|
22
|
+
when 'slack'
|
23
|
+
Slack.new.notify(message)
|
24
|
+
when 'webhook'
|
25
|
+
p 'webhook'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Notification
|
2
|
+
class Slack
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
require 'net/https'
|
6
|
+
def initialize
|
7
|
+
@slack_secret = ENV['SLACK_SECRET']
|
8
|
+
@slack_channel = ENV['SLACK_CHANNEL']
|
9
|
+
@slack_base_url ='https://slack.com/api/'
|
10
|
+
end
|
11
|
+
|
12
|
+
def notify(message)
|
13
|
+
return if @slack_secret.nil? || @slack_channel.nil?
|
14
|
+
uri = URI.parse("#{@slack_base_url}chat.postMessage")
|
15
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
16
|
+
http.use_ssl = true
|
17
|
+
request = Net::HTTP::Post.new(uri.request_uri, initHeader = {'Content-Type' =>'application/json', 'Authorization' => 'Bearer ' + @slack_secret})
|
18
|
+
request.body = {
|
19
|
+
channel: @slack_channel,
|
20
|
+
text: message
|
21
|
+
}.to_json
|
22
|
+
response = http.request(request)
|
23
|
+
puts response.body
|
24
|
+
end
|
25
|
+
|
26
|
+
def backup_notification(result, date, database, backup_path)
|
27
|
+
return if @slack_secret.nil? || @slack_channel.nil?
|
28
|
+
uri = URI.parse("#{@slack_base_url}chat.postMessage")
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
30
|
+
http.use_ssl = true
|
31
|
+
request = Net::HTTP::Post.new(uri.request_uri, initHeader = {'Content-Type' =>'application/json', 'Authorization' => 'Bearer ' + @slack_secret})
|
32
|
+
message_one = "Backup of #{database} successfully finished at #{Time.now}"
|
33
|
+
message_two = "Backup path:\`#{backup_path}/#{database}_#{date}.dump\`"
|
34
|
+
data = {
|
35
|
+
channel: @slack_channel,
|
36
|
+
blocks: [
|
37
|
+
{
|
38
|
+
type: 'header',
|
39
|
+
text: {
|
40
|
+
type: 'plain_text',
|
41
|
+
text: ENV['DEFAULT_URL'] || "#{database} Backup",
|
42
|
+
emoji: true
|
43
|
+
}
|
44
|
+
},
|
45
|
+
{
|
46
|
+
type: 'section',
|
47
|
+
text: {
|
48
|
+
type: 'mrkdwn',
|
49
|
+
text: result ? "#{message_one}\n#{message_two}" : "Backup of #{database} failed at #{Time.now}"
|
50
|
+
}
|
51
|
+
}
|
52
|
+
]
|
53
|
+
}
|
54
|
+
request.body = data.to_json
|
55
|
+
begin
|
56
|
+
response = JSON.parse(http.request(request).body)
|
57
|
+
if response['ok'] == false
|
58
|
+
raise Notification::Error, response['error']
|
59
|
+
end
|
60
|
+
response
|
61
|
+
rescue => e
|
62
|
+
puts "Slack error: \n\t#{e.message}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Notification
|
2
|
+
class Webhook
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
require 'net/https'
|
6
|
+
require 'openssl'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@webhook_url = ENV['WEBHOOK_URL']
|
11
|
+
@secret = ENV['WEBHOOK_SECRET']
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate_signature(payload_body)
|
15
|
+
"md5=#{OpenSSL::HMAC.hexdigest('md5', ENV['WEBHOOK_SECRET'], payload_body)}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def backup_notification(result, date, database, backup_path)
|
19
|
+
return if @webhook_url.nil? || @secret.nil?
|
20
|
+
|
21
|
+
data = {
|
22
|
+
domain: ENV['DEFAULT_URL'] || "#{database} Backup",
|
23
|
+
backupPath: result ? backup_path : nil,
|
24
|
+
backupDate: date,
|
25
|
+
}.to_json
|
26
|
+
|
27
|
+
uri = URI.parse(@webhook_url)
|
28
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
29
|
+
https.use_ssl = uri.scheme == "https"
|
30
|
+
request = Net::HTTP::Post.new(uri.path.empty? ? "/" : uri.path, initHeader = {'Content-Type' =>'application/json', 'x-hub-signature' => generate_signature("#{data}")})
|
31
|
+
request.body = "#{data}"
|
32
|
+
begin
|
33
|
+
response = https.request(request)
|
34
|
+
response.to_hash
|
35
|
+
rescue => e
|
36
|
+
puts "Webhook error: \n\t#{e.message}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capistrano/ops'
|
4
|
+
require 'rails'
|
5
|
+
module Capistrano
|
6
|
+
module Ops
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
railtie_name :ops
|
9
|
+
rake_tasks do
|
10
|
+
path = File.expand_path(__dir__)
|
11
|
+
Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
namespace :pg do
|
5
|
+
default_backup_path = Rails.env.development? ? 'tmp/backups' : '../../shared/backups'
|
6
|
+
database = Rails.configuration.database_configuration[Rails.env]['database']
|
7
|
+
username = Rails.configuration.database_configuration[Rails.env]['username']
|
8
|
+
password = Rails.configuration.database_configuration[Rails.env]['password']
|
9
|
+
hostname = Rails.configuration.database_configuration[Rails.env]['host']
|
10
|
+
portnumber = Rails.configuration.database_configuration[Rails.env]['port']
|
11
|
+
backup_path = Rails.root.join(default_backup_path).to_s
|
12
|
+
backups_enabled = Rails.env.production? || ENV['BACKUPS_ENABLED'] == 'true'
|
13
|
+
|
14
|
+
task :dump do
|
15
|
+
api = Notification::Api.new
|
16
|
+
date = Time.now.to_i
|
17
|
+
user = username.present? ? " -U #{username}" : ''
|
18
|
+
host = hostname.present? ? " -h #{hostname}" : ''
|
19
|
+
port = portnumber.present? ? " -p #{portnumber}" : ''
|
20
|
+
# rubocop:disable Layout/LineLength
|
21
|
+
dump_cmd = "export PGPASSWORD='#{password}' && cd #{backup_path} && pg_dump -d #{database}#{user}#{host}#{port} > #{database}_#{date}.dump"
|
22
|
+
# rubocop:enable Layout/LineLength
|
23
|
+
if backups_enabled
|
24
|
+
system "mkdir -p #{backup_path}" unless Dir.exist?(backup_path)
|
25
|
+
result = system(dump_cmd)
|
26
|
+
api.send_backup_notification(result, date, database, backup_path)
|
27
|
+
# Notification::Slack.new.backup_notification(result, date, database, backup_path)
|
28
|
+
end
|
29
|
+
if backups_enabled
|
30
|
+
# rubocop:disable Layout/LineLength
|
31
|
+
p result ? "Backup created: #{backup_path}/#{database}_#{date}.dump" : "Backup failed, created empty file at #{backup_path}/#{database}_#{date}.dump"
|
32
|
+
system "rm #{backup_path}/#{database}_#{date}.dump" unless result
|
33
|
+
# rubocop:enable Layout/LineLength
|
34
|
+
else
|
35
|
+
p 'dump: Backups are disabled'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :pg do
|
4
|
+
default_backup_path = Rails.env.development? ? 'tmp/backups' : '../../shared/backups'
|
5
|
+
database = Rails.configuration.database_configuration[Rails.env]['database']
|
6
|
+
|
7
|
+
backup_path = Rails.root.join(default_backup_path).to_s
|
8
|
+
backups_enabled = Rails.env.production? || ENV['BACKUPS_ENABLED'] == 'true'
|
9
|
+
|
10
|
+
task :remove_old_dumps do
|
11
|
+
bash_regex = "'#{database}.{0,}\.dump'"
|
12
|
+
total_backups_no = ENV['NUMBER_OF_BACKUPS'] || 1
|
13
|
+
# rubocop:disable Layout/LineLength
|
14
|
+
cmd = "cd #{backup_path} && ls -lt | grep -E -i #{bash_regex} | tail -n +#{total_backups_no.to_i + 1} | awk '{print $9}'|xargs rm -rf"
|
15
|
+
# rubocop:enable Layout/LineLength
|
16
|
+
system(cmd) if backups_enabled
|
17
|
+
p backups_enabled ? 'Old backups removed' : 'remove_old_dumps: Backups are disabled'
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Ops
|
5
|
+
require 'capistrano/ops/notification'
|
6
|
+
require 'capistrano/ops/railtie' if defined?(Rails)
|
7
|
+
require 'capistrano/ops/capistrano' if defined?(Capistrano::VERSION)
|
8
|
+
def self.path
|
9
|
+
Dir.pwd
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.bin_rails?
|
13
|
+
File.exist?(File.join(path, 'bin', 'rails'))
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.script_rails?
|
17
|
+
File.exist?(File.join(path, 'script', 'rails'))
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.bundler?
|
21
|
+
File.exist?(File.join(path, 'Gemfile'))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: capistrano-ops
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Florian Crusius
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-03-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.3.9
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.3.9
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description: A collection of devops tasks for rails applications
|
42
|
+
email:
|
43
|
+
- florian@zauberware.com
|
44
|
+
executables:
|
45
|
+
- console
|
46
|
+
- setup
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- ".gitignore"
|
51
|
+
- ".travis.yml"
|
52
|
+
- Gemfile
|
53
|
+
- LICENSE.txt
|
54
|
+
- README.md
|
55
|
+
- Rakefile
|
56
|
+
- bin/console
|
57
|
+
- bin/setup
|
58
|
+
- capistrano-ops.gemspec
|
59
|
+
- lib/capistrano/ops.rb
|
60
|
+
- lib/capistrano/ops/capistrano.rb
|
61
|
+
- lib/capistrano/ops/capistrano/v3/tasks/backup.rake
|
62
|
+
- lib/capistrano/ops/capistrano/v3/tasks/figaro_yml.rake
|
63
|
+
- lib/capistrano/ops/capistrano/v3/tasks/invoke.rake
|
64
|
+
- lib/capistrano/ops/capistrano/v3/tasks/logs.rake
|
65
|
+
- lib/capistrano/ops/capistrano/v3/tasks/whenever.rake
|
66
|
+
- lib/capistrano/ops/notification.rb
|
67
|
+
- lib/capistrano/ops/notification/api.rb
|
68
|
+
- lib/capistrano/ops/notification/slack.rb
|
69
|
+
- lib/capistrano/ops/notification/webhook.rb
|
70
|
+
- lib/capistrano/ops/railtie.rb
|
71
|
+
- lib/capistrano/ops/tasks/pg/dump.rake
|
72
|
+
- lib/capistrano/ops/tasks/pg/remove_old_dumps.rake
|
73
|
+
- lib/capistrano/ops/version.rb
|
74
|
+
homepage: https://github.com/zauberware/capistrano-ops
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata: {}
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubygems_version: 3.3.26
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: devops tasks for rails applications
|
97
|
+
test_files: []
|