travis-backup 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dca7a53c9e3689c5f0988d074296449769a106b86ed6ddc26c6a8b7c0cba15b2
4
- data.tar.gz: 25765ff21af6e3bbba2fbf5a971d678acd6447b59c54fc685ddaa0912e84f73a
3
+ metadata.gz: da5ede447a9a2a1b215d03d8582f62e8c4573e69e3337da91afaaffef07323f3
4
+ data.tar.gz: 01bc3c92f48f093ae3dd4ff23c07a0f2e9c5516b79c05279700d82570b0a2d40
5
5
  SHA512:
6
- metadata.gz: bcc37fc6219ac84eda44f953879952567eb33981a9a2436b5922acefa515994262ab174e25e8fe9660153e73e3d55838eaa8fd375dc8f79cefff02f574f8e516
7
- data.tar.gz: 377cef9823245bd5153bc291e6fb078c723544986e6fd85a79e50b64d8cc5110c1331ac8247a40b158022633672ad93e3a85719d2b70f4659e152913f0b4b729
6
+ metadata.gz: 775e8b95e35be972da5194d703d33645bfe21198d91f28efe2a417214d74e4fc71fee5a9c6d7a715ae2819b8d12a0b0bad691bca44b507782acd1dbf48c43d16
7
+ data.tar.gz: 289107dfc9d91b3cc43479c1a4973fe0fec4db91a519dd9cd8be321c9ce56f65e2170464ae3c88492385af5609d0ea52bfbe9d7510f1a44bcffa8f1dfea81b63
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- travis-backup (0.0.3)
4
+ travis-backup (0.1.0)
5
5
  activerecord
6
6
  bootsnap
7
7
  pg
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # README
2
2
 
3
- *travis-backup* is an application that removes builds and their corresponding jobs
4
- and exports them (optionally) to json files.
3
+ *travis-backup* is an application that helps with housekeeping and backup for Travis CI database v2.2 and with migration to v3.0 database.
5
4
 
6
5
  ### Installation and run
7
6
 
@@ -27,6 +26,9 @@ All arguments:
27
26
  -u, --user_id ID # run only for given user
28
27
  -o, --org_id ID # run only for given organization
29
28
  -r, --repo_id ID # run only for given repository
29
+ --move_logs # run in move logs mode - move all logs to database at destination_db_url URL
30
+ --destination_db_url URL # URL for moving logs to
31
+ --remove_orphans # run in remove orphans mode
30
32
  ```
31
33
 
32
34
  Or inside your app:
@@ -54,9 +56,17 @@ backup.run(org_id: 1)
54
56
  backup.run(repo_id: 1)
55
57
  ```
56
58
 
57
- ### Configuration
59
+ #### Special modes
58
60
 
59
- Despite of command line arguments, one of the ways you can configure your export is a file `config/settinigs.yml` that you can place in your app's main directory. The gem expects properties in the following format:
61
+ Using `--move_logs` flag you can move all logs to database at `destination_db_url` URL (which is required in this case). When you run gem in this mode no files are created and no other tables are being touched.
62
+
63
+ Using `--remove_orphans` flag you can remove all orphaned data from tables. When you run gem in this mode no files are created.
64
+
65
+ Using `--dry_run` flag you can check which data would be removed by gem, but without removing them actually. Instead of that reports will be printed on standard output. This flag can be also combined with `--move_logs` or `--remove_orphans`.
66
+
67
+ ### Configuration options
68
+
69
+ Despite of command line arguments, one of the ways you can configure your export is a file `config/settings.yml` that you can place in your app's main directory. The gem expects properties in the following format:
60
70
 
61
71
  ```
62
72
  backup:
@@ -88,9 +98,9 @@ and
88
98
  bundle exec rspec
89
99
  ```
90
100
 
91
- To make tests working properly you should also ensure the database connection string for an empty test database. You can set it as `DATABASE_URL` environment variable or in `config/database.yml`.
101
+ To make tests working properly you should also ensure database connection strings for empty test databases. You can set them as `DATABASE_URL` and `BACKUP_DESTINATION_DB_URL` environment variables or in `config/database.yml`.
92
102
 
93
- **Warning: this database will be cleaned during tests, so ensure that it includes no important data.**
103
+ **Warning: these databases will be cleaned during tests, so ensure that they include no important data.**
94
104
 
95
105
  ### Ruby version
96
106
 
data/lib/config.rb CHANGED
@@ -12,6 +12,7 @@ class Config
12
12
  :repo_id,
13
13
  :org_id,
14
14
  :move_logs,
15
+ :remove_orphans,
15
16
  :destination_db_url
16
17
 
17
18
  def initialize(args={}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
@@ -88,6 +89,13 @@ class Config
88
89
  config.dig('backup', 'move_logs'),
89
90
  false
90
91
  )
92
+ @remove_orphans = first_not_nil(
93
+ args[:remove_orphans],
94
+ argv_opts[:remove_orphans],
95
+ ENV['BACKUP_REMOVE_ORPHANS'],
96
+ config.dig('backup', 'remove_orphans'),
97
+ false
98
+ )
91
99
  @destination_db_url = first_not_nil(
92
100
  args[:destination_db_url],
93
101
  argv_opts[:destination_db_url],
@@ -97,7 +105,7 @@ class Config
97
105
  end
98
106
 
99
107
  def check_values
100
- if !@move_logs && !@threshold
108
+ if !@move_logs && !@remove_orphans && !@threshold
101
109
  message = abort_message("Please provide the threshold argument. Data younger than it will be omitted. " +
102
110
  "Threshold defines number of months from now.")
103
111
  abort message
@@ -138,6 +146,7 @@ class Config
138
146
  opt.on('-r', '--repo_id X') { |o| options[:repo_id] = o.to_i }
139
147
  opt.on('-o', '--org_id X') { |o| options[:org_id] = o.to_i }
140
148
  opt.on('--move_logs') { |o| options[:move_logs] = o }
149
+ opt.on('--remove_orphans') { |o| options[:remove_orphans] = o }
141
150
  opt.on('--destination_db_url X') { |o| options[:destination_db_url] = o }
142
151
  end.parse!
143
152
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+
5
+ class Branch < Model
6
+ self.table_name = 'branches'
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+
5
+ class Commit < Model
6
+ self.table_name = 'commits'
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+
5
+ class Cron < Model
6
+ self.table_name = 'crons'
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+
5
+ class PullRequest < Model
6
+ self.table_name = 'pull_requests'
7
+ end
@@ -2,10 +2,12 @@
2
2
 
3
3
  require 'models/model'
4
4
  require 'models/build'
5
+ require 'models/request'
5
6
 
6
7
  # Repository model
7
8
  class Repository < Model
8
9
  has_many :builds, -> { order('id') }, foreign_key: :repository_id, dependent: :destroy, class_name: 'Build'
10
+ has_many :requests, -> { order('id') }, foreign_key: :repository_id, dependent: :destroy, class_name: 'Request'
9
11
 
10
12
  self.table_name = 'repositories'
11
13
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+ require 'models/repository'
5
+
6
+ class Request < Model
7
+ belongs_to :repository
8
+
9
+ self.table_name = 'requests'
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+
5
+ class SslKey < Model
6
+ self.table_name = 'ssl_keys'
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+
5
+ class Stage < Model
6
+ self.table_name = 'stages'
7
+ end
data/lib/models/tag.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'models/model'
4
+
5
+ class Tag < Model
6
+ self.table_name = 'tags'
7
+ end
data/lib/travis-backup.rb CHANGED
@@ -5,17 +5,25 @@ require 'active_support/time'
5
5
  require 'config'
6
6
  require 'models/repository'
7
7
  require 'models/log'
8
+ require 'models/branch'
9
+ require 'models/tag'
10
+ require 'models/commit'
11
+ require 'models/cron'
12
+ require 'models/pull_request'
13
+ require 'models/ssl_key'
14
+ require 'models/request'
15
+ require 'models/stage'
8
16
 
9
17
  # main travis-backup class
10
18
  class Backup
11
19
  attr_accessor :config
12
- attr_reader :dry_run_removed
20
+ attr_reader :dry_run_report
13
21
 
14
22
  def initialize(config_args={})
15
23
  @config = Config.new(config_args)
16
24
 
17
25
  if @config.dry_run
18
- @dry_run_removed = {builds: [], jobs: [], logs: []}
26
+ @dry_run_report = {builds: [], jobs: [], logs: [], requests: []}
19
27
  end
20
28
 
21
29
  connect_db
@@ -26,8 +34,6 @@ class Backup
26
34
  end
27
35
 
28
36
  def run(args={})
29
- return move_logs if @config.move_logs
30
-
31
37
  user_id = args[:user_id] || @config.user_id
32
38
  repo_id = args[:repo_id] || @config.repo_id
33
39
  org_id = args[:org_id] || @config.org_id
@@ -38,32 +44,80 @@ class Backup
38
44
  elsif org_id
39
45
  owner_id = org_id
40
46
  owner_type = 'Organization'
41
- elsif repo_id
42
- repo_id = repo_id
43
47
  end
44
48
 
45
- if owner_id
46
- Repository.where('owner_id = ? and owner_type = ?', owner_id, owner_type).order(:id).each do |repository|
47
- process_repo(repository)
48
- end
49
+ if @config.move_logs
50
+ move_logs
51
+ elsif @config.remove_orphans
52
+ remove_orphans
53
+ elsif owner_id
54
+ process_repos_for_owner(owner_id, owner_type)
49
55
  elsif repo_id
50
- repository = Repository.find(repo_id)
56
+ process_repo_with_id(repo_id)
57
+ else
58
+ process_all_repos
59
+ end
60
+
61
+ print_dry_run_report if @config.dry_run
62
+ end
63
+
64
+ def process_repos_for_owner(owner_id, owner_type)
65
+ Repository.where('owner_id = ? and owner_type = ?', owner_id, owner_type).order(:id).each do |repository|
51
66
  process_repo(repository)
67
+ end
68
+ end
69
+
70
+ def process_repo_with_id(repo_id)
71
+ process_repo(Repository.find(repo_id))
72
+ end
73
+
74
+ def process_all_repos
75
+ Repository.order(:id).each do |repository|
76
+ process_repo(repository)
77
+ end
78
+ end
79
+
80
+ def print_dry_run_report
81
+ if @dry_run_report.to_a.map(&:second).flatten.empty?
82
+ puts 'Dry run active. No data would be removed in normal run.'
52
83
  else
53
- Repository.order(:id).each do |repository|
54
- process_repo(repository)
84
+ puts 'Dry run active. The following data would be removed in normal run:'
85
+
86
+ @dry_run_report.to_a.map(&:first).each do |symbol|
87
+ print_dry_run_report_line(symbol)
55
88
  end
56
89
  end
90
+ end
57
91
 
58
- if @config.dry_run
59
- puts 'Dry run active. The following data would be removed in normal mode:'
60
- puts " - builds: #{@dry_run_removed[:builds].to_json}"
61
- puts " - jobs: #{@dry_run_removed[:jobs].to_json}"
62
- puts " - logs: #{@dry_run_removed[:logs].to_json}"
63
- end
92
+ def print_dry_run_report_line(symbol)
93
+ puts " - #{symbol}: #{@dry_run_report[symbol].to_json}" if @dry_run_report[symbol].any?
94
+ end
95
+
96
+ def process_repo(repository)
97
+ process_repo_builds(repository)
98
+ process_repo_requests(repository)
99
+ end
100
+
101
+ def process_repo_builds(repository) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
102
+ threshold = @config.threshold.to_i.months.ago.to_datetime
103
+ current_build_id = repository.current_build_id || -1
104
+ repository.builds.where('created_at < ? and id != ?', threshold, current_build_id)
105
+ .in_groups_of(@config.limit.to_i, false).map do |builds_batch|
106
+ @config.if_backup ? save_and_destroy_builds_batch(builds_batch, repository) : destroy_builds_batch(builds_batch)
107
+ end.compact
108
+ end
109
+
110
+ def process_repo_requests(repository)
111
+ threshold = @config.threshold.to_i.months.ago.to_datetime
112
+ repository.requests.where('created_at < ?', threshold)
113
+ .in_groups_of(@config.limit.to_i, false).map do |requests_batch|
114
+ @config.if_backup ? save_and_destroy_requests_batch(requests_batch, repository) : destroy_requests_batch(requests_batch)
115
+ end.compact
64
116
  end
65
117
 
66
118
  def move_logs
119
+ return move_logs_dry if config.dry_run
120
+
67
121
  connect_db(@config.database_url)
68
122
  Log.order(:id).in_groups_of(@config.limit.to_i, false).map do |logs_batch|
69
123
  log_hashes = logs_batch.as_json
@@ -80,41 +134,86 @@ class Backup
80
134
  end
81
135
  end
82
136
 
83
- def process_repo(repository) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
84
- threshold = @config.threshold.to_i.months.ago.to_datetime
85
- current_build_id = repository.current_build_id || -1
86
- repository.builds.where('created_at < ? and id != ?', threshold, current_build_id)
87
- .in_groups_of(@config.limit.to_i, false).map do |builds_batch|
88
- @config.if_backup ? save_and_destroy_batch(builds_batch, repository) : destroy_batch(builds_batch)
89
- end.compact
137
+ def move_logs_dry
138
+ dry_run_report[:logs].concat(Log.order(:id).map(&:id))
139
+ end
140
+
141
+ def remove_orphans
142
+ remove_orphans_for_table(Repository, 'repositories', 'builds', 'current_build_id')
143
+ remove_orphans_for_table(Repository, 'repositories', 'builds', 'last_build_id')
144
+ remove_orphans_for_table(Build, 'builds', 'repositories', 'repository_id')
145
+ remove_orphans_for_table(Build, 'builds', 'commits', 'commit_id')
146
+ remove_orphans_for_table(Build, 'builds', 'requests', 'request_id')
147
+ remove_orphans_for_table(Build, 'builds', 'pull_requests', 'pull_request_id')
148
+ remove_orphans_for_table(Build, 'builds', 'branches', 'branch_id')
149
+ remove_orphans_for_table(Build, 'builds', 'tags', 'tag_id')
150
+ remove_orphans_for_table(Job, 'jobs', 'repositories', 'repository_id')
151
+ remove_orphans_for_table(Job, 'jobs', 'commits', 'commit_id')
152
+ remove_orphans_for_table(Job, 'jobs', 'stages', 'stage_id')
153
+ remove_orphans_for_table(Branch, 'branches', 'repositories', 'repository_id')
154
+ remove_orphans_for_table(Branch, 'branches', 'builds', 'last_build_id')
155
+ remove_orphans_for_table(Tag, 'tags', 'repositories', 'repository_id')
156
+ remove_orphans_for_table(Tag, 'tags', 'builds', 'last_build_id')
157
+ remove_orphans_for_table(Commit, 'commits', 'repositories', 'repository_id')
158
+ remove_orphans_for_table(Commit, 'commits', 'branches', 'branch_id')
159
+ remove_orphans_for_table(Commit, 'commits', 'tags', 'tag_id')
160
+ remove_orphans_for_table(Cron, 'crons', 'branches', 'branch_id')
161
+ remove_orphans_for_table(PullRequest, 'pull_requests', 'repositories', 'repository_id')
162
+ remove_orphans_for_table(SslKey, 'ssl_keys', 'repositories', 'repository_id')
163
+ remove_orphans_for_table(Request, 'requests', 'commits', 'commit_id')
164
+ remove_orphans_for_table(Request, 'requests', 'pull_requests', 'pull_request_id')
165
+ remove_orphans_for_table(Request, 'requests', 'branches', 'branch_id')
166
+ remove_orphans_for_table(Request, 'requests', 'tags', 'tag_id')
167
+ remove_orphans_for_table(Stage, 'stages', 'builds', 'build_id')
168
+ end
169
+
170
+ def remove_orphans_for_table(model_class, table_a_name, table_b_name, fk_name)
171
+ for_delete = model_class.find_by_sql(%{
172
+ select a.*
173
+ from #{table_a_name} a
174
+ left join #{table_b_name} b
175
+ on a.#{fk_name} = b.id
176
+ where
177
+ a.#{fk_name} is not null
178
+ and b.id is null;
179
+ })
180
+
181
+ if config.dry_run
182
+ key = table_a_name.to_sym
183
+ dry_run_report[key] = [] if dry_run_report[key].nil?
184
+ dry_run_report[key].concat(for_delete.map(&:id))
185
+ dry_run_report[key].uniq!
186
+ else
187
+ model_class.where(id: for_delete.map(&:id)).delete_all
188
+ end
90
189
  end
91
190
 
92
191
  private
93
192
 
94
- def save_and_destroy_batch(builds_batch, repository)
193
+ def save_and_destroy_builds_batch(builds_batch, repository)
95
194
  builds_export = export_builds(builds_batch)
96
195
  file_name = "repository_#{repository.id}_builds_#{builds_batch.first.id}-#{builds_batch.last.id}.json"
97
196
  pretty_json = JSON.pretty_generate(builds_export)
98
197
  if save_file(file_name, pretty_json)
99
- destroy_batch(builds_batch)
198
+ destroy_builds_batch(builds_batch)
100
199
  end
101
200
  builds_export
102
201
  end
103
202
 
104
- def destroy_batch(builds_batch)
105
- return destroy_batch_dry(builds_batch) if @config.dry_run
203
+ def destroy_builds_batch(builds_batch)
204
+ return destroy_builds_batch_dry(builds_batch) if @config.dry_run
106
205
 
107
206
  builds_batch.each(&:destroy)
108
207
  end
109
208
 
110
- def destroy_batch_dry(builds_batch)
111
- @dry_run_removed[:builds].concat(builds_batch.map(&:id))
209
+ def destroy_builds_batch_dry(builds_batch)
210
+ @dry_run_report[:builds].concat(builds_batch.map(&:id))
112
211
 
113
212
  jobs = builds_batch.map do |build|
114
213
  build.jobs.map(&:id) || []
115
214
  end.flatten
116
215
 
117
- @dry_run_removed[:jobs].concat(jobs)
216
+ @dry_run_report[:jobs].concat(jobs)
118
217
 
119
218
  logs = builds_batch.map do |build|
120
219
  build.jobs.map do |job|
@@ -122,7 +221,27 @@ class Backup
122
221
  end.flatten || []
123
222
  end.flatten
124
223
 
125
- @dry_run_removed[:logs].concat(logs)
224
+ @dry_run_report[:logs].concat(logs)
225
+ end
226
+
227
+ def save_and_destroy_requests_batch(requests_batch, repository)
228
+ requests_export = export_requests(requests_batch)
229
+ file_name = "repository_#{repository.id}_requests_#{requests_batch.first.id}-#{requests_batch.last.id}.json"
230
+ pretty_json = JSON.pretty_generate(requests_export)
231
+ if save_file(file_name, pretty_json)
232
+ destroy_requests_batch(requests_batch)
233
+ end
234
+ requests_export
235
+ end
236
+
237
+ def destroy_requests_batch(requests_batch)
238
+ return destroy_requests_batch_dry(requests_batch) if @config.dry_run
239
+
240
+ requests_batch.each(&:destroy)
241
+ end
242
+
243
+ def destroy_requests_batch_dry(requests_batch)
244
+ @dry_run_report[:requests].concat(requests_batch.map(&:id))
126
245
  end
127
246
 
128
247
  def save_file(file_name, content) # rubocop:disable Metrics/MethodLength
@@ -169,9 +288,13 @@ class Backup
169
288
 
170
289
  def export_logs(logs)
171
290
  logs.map do |log|
172
- log_export = log.attributes
291
+ log.attributes
292
+ end
293
+ end
173
294
 
174
- log_export
295
+ def export_requests(requests)
296
+ requests.map do |request|
297
+ request.attributes
175
298
  end
176
299
  end
177
300
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'travis-backup'
3
- s.version = '0.0.3'
3
+ s.version = '0.1.0'
4
4
  s.summary = 'Travis CI backup tool'
5
5
  s.authors = ['Karol Selak']
6
6
  s.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: travis-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Selak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-02 00:00:00.000000000 Z
11
+ date: 2021-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -248,12 +248,20 @@ files:
248
248
  - db/schema.sql
249
249
  - dump/.keep
250
250
  - lib/config.rb
251
+ - lib/models/branch.rb
251
252
  - lib/models/build.rb
253
+ - lib/models/commit.rb
254
+ - lib/models/cron.rb
252
255
  - lib/models/job.rb
253
256
  - lib/models/log.rb
254
257
  - lib/models/model.rb
255
258
  - lib/models/organization.rb
259
+ - lib/models/pull_request.rb
256
260
  - lib/models/repository.rb
261
+ - lib/models/request.rb
262
+ - lib/models/ssl_key.rb
263
+ - lib/models/stage.rb
264
+ - lib/models/tag.rb
257
265
  - lib/models/user.rb
258
266
  - lib/travis-backup.rb
259
267
  - log/.keep