pdksync 0.5.0 → 0.6.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.
@@ -0,0 +1,1293 @@
1
+ # @summary provides a module with various methods for performing the desired tasks
2
+ require 'git'
3
+ require 'open3'
4
+ require 'fileutils'
5
+ require 'pdk'
6
+ require 'pdksync/configuration'
7
+ require 'pdksync/gitplatformclient'
8
+ require 'colorize'
9
+ require 'bundler'
10
+ require 'octokit'
11
+ require 'pdk/util/template_uri'
12
+ require 'pdksync/logger'
13
+
14
+ module PdkSync
15
+ module Utils
16
+ def self.configuration
17
+ @configuration ||= PdkSync::Configuration.new
18
+ end
19
+
20
+ def self.on_windows?
21
+ # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
22
+ # library uses that to test what platform it's on.
23
+ !!File::ALT_SEPARATOR # rubocop:disable Style/DoubleNegation
24
+ end
25
+
26
+ def self.temp_file_path
27
+ @temp_file_path ||= on_windows? ? "#{ENV['TEMP']}\\out.tmp" : '/tmp/out.tmp'
28
+ end
29
+
30
+ # @summary
31
+ # This method when called will delete any preexisting branch on the given repository that matches the given name.
32
+ # @param [PdkSync::GitPlatformClient] client
33
+ # The Git platform client used to gain access to and manipulate the repository.
34
+ # @param [String] repo_name
35
+ # The name of the repository from which the branch is to be deleted.
36
+ # @param [String] branch_name
37
+ # The name of the branch that is to be deleted.
38
+ def self.delete_branch(client, repo_name, branch_name)
39
+ client.delete_branch(repo_name, branch_name)
40
+ rescue StandardError => error
41
+ PdkSync::Logger.fatal "Deleting #{branch_name} in #{repo_name} failed. #{error}"
42
+ end
43
+
44
+ # @summary
45
+ # This method when called will add a given label to a given repository
46
+ # @param [PdkSync::GitPlatformClient] client
47
+ # The Git Platform client used to gain access to and manipulate the repository.
48
+ # @param [String] repo_name
49
+ # The name of the repository on which the commit is to be made.
50
+ # @param [Integer] issue_number
51
+ # The id of the issue (i.e. pull request) to add the label to.
52
+ # @param [String] label
53
+ # The label to add.
54
+ def self.add_label(client, repo_name, issue_number, label)
55
+ client.update_issue(repo_name, issue_number, labels: [label])
56
+ rescue StandardError => error
57
+ PdkSync::Logger.info "Adding label to #{repo_name} issue #{issue_number} has failed. #{error}"
58
+ return false
59
+ end
60
+
61
+ # @summary
62
+ # This method when called will check on the given repository for the existence of the supplied label
63
+ # @param [PdkSync::GitPlatformClient] client
64
+ # The Git platform client used to gain access to and manipulate the repository.
65
+ # @param [String] repo_name
66
+ # The name of the repository on which the commit is to be made.
67
+ # @param [String] label
68
+ # The label to check for.
69
+ # @return [Boolean]
70
+ # A boolean stating whether the label was found.
71
+ def self.check_for_label(client, repo_name, label)
72
+ # Get labels from repository
73
+ repo_labels = client.labels(repo_name)
74
+
75
+ # Look for label in the repository's labels
76
+ match = false
77
+ repo_labels.each do |repo_label|
78
+ if repo_label.name == label
79
+ match = true
80
+ break
81
+ end
82
+ end
83
+
84
+ # Raise error if a match was not found else return true
85
+ (match == false) ? (raise StandardError, "Label '#{label}' not found in #{repo_name}") : (return true)
86
+ rescue StandardError => error
87
+ PdkSync::Logger.fatal "Retrieving labels for #{repo_name} has failed. #{error}"
88
+ return false
89
+ end
90
+
91
+ # @summary
92
+ # This method when called will retrieve the pdk_version of the current module, i.e. the one that was navigated into in the 'pdk_update' method.
93
+ # @param [String] metadata_file
94
+ # An optional input that can be used to set the location of the metadata file.
95
+ # @return [String]
96
+ # A string value that represents the current pdk version.
97
+ def self.return_pdk_version(metadata_file = 'metadata.json')
98
+ file = File.read(metadata_file)
99
+ data_hash = JSON.parse(file)
100
+ data_hash['pdk-version']
101
+ end
102
+
103
+ # @summary
104
+ # This method when called will stage all changed files within the given repository, conditional on them being managed via the pdk.
105
+ # @param [Git::Base] git_repo
106
+ # A git object representing the local repository to be staged.
107
+ def self.add_staged_files(git_repo)
108
+ if !git_repo.status.changed.empty?
109
+ git_repo.add(all: true)
110
+ PdkSync::Logger.info 'All files have been staged.'
111
+ true
112
+ else
113
+ PdkSync::Logger.info 'Nothing to commit.'
114
+ false
115
+ end
116
+ end
117
+
118
+ # @summary
119
+ # This method when called will create a commit containing all currently staged files, with the name of the commit containing the template ref as a unique identifier.
120
+ # @param [Git::Base] git_repo
121
+ # A git object representing the local repository against which the commit is to be made.
122
+ # @param [String] template_ref
123
+ # The unique template_ref that is used as part of the commit name.
124
+ # @param [String] commit_message
125
+ # If specified it will be the message for the commit.
126
+ def self.commit_staged_files(git_repo, template_ref, commit_message = nil)
127
+ message = if commit_message.nil?
128
+ "pdksync_#{template_ref}"
129
+ else
130
+ commit_message
131
+ end
132
+ git_repo.commit(message)
133
+ end
134
+
135
+ # @summary
136
+ # This method when called will push the given local commit to local repository's origin.
137
+ # @param [Git::Base] git_repo
138
+ # A git object representing the local repository againt which the push is to be made.
139
+ # @param [String] template_ref
140
+ # The unique reference that that represents the template the update has ran against.
141
+ # @param [String] repo_name
142
+ # The name of the repository on which the commit is to be made.
143
+ def self.push_staged_files(git_repo, current_branch, repo_name)
144
+ git_repo.push(configuration.push_file_destination, current_branch)
145
+ rescue StandardError => error
146
+ PdkSync::Logger.error "Pushing to #{configuration.push_file_destination} for #{repo_name} has failed. #{error}"
147
+ end
148
+
149
+ # @summary
150
+ # This method when called will create a pr on the given repository that will create a pr to merge the given commit into the main with the pdk version as an identifier.
151
+ # @param [PdkSync::GitPlatformClient] client
152
+ # The Git platform client used to gain access to and manipulate the repository.
153
+ # @param [String] repo_name
154
+ # The name of the repository on which the commit is to be made.
155
+ # @param [String] template_ref
156
+ # The unique reference that that represents the template the update has ran against.
157
+ # @param [String] pdk_version
158
+ # The current version of the pdk on which the update is run.
159
+ def self.create_pr(client, repo_name, template_ref, pdk_version, pr_title = nil)
160
+ if pr_title.nil?
161
+ title = "pdksync - Update using #{pdk_version}"
162
+ message = "pdk version: `#{pdk_version}` \n pdk template ref: `#{template_ref}`"
163
+ head = "pdksync_#{template_ref}"
164
+ else
165
+ title = "pdksync - #{pr_title}"
166
+ message = "#{pr_title}\npdk version: `#{pdk_version}` \n"
167
+ head = template_ref
168
+ end
169
+ client.create_pull_request(repo_name, configuration.create_pr_against,
170
+ head,
171
+ title,
172
+ message)
173
+ rescue StandardError => error
174
+ PdkSync::Logger.fatal "PR creation for #{repo_name} has failed. #{error}"
175
+ nil
176
+ end
177
+
178
+ # @summary
179
+ # Try to use a fully installed pdk, otherwise fall back to the bundled pdk gem.
180
+ # @return String
181
+ # Path to the pdk executable
182
+ def self.return_pdk_path
183
+ full_path = '/opt/puppetlabs/pdk/bin/pdk'
184
+ path = if File.executable?(full_path)
185
+ full_path
186
+ else
187
+ PdkSync::Logger.warn "(WARNING) Using pdk on PATH not '#{full_path}'"
188
+ 'pdk'
189
+ end
190
+ path
191
+ end
192
+
193
+ # @return
194
+ def self.create_commit(git_repo, branch_name, commit_message)
195
+ checkout_branch(git_repo, branch_name)
196
+ commit_staged_files(git_repo, branch_name, commit_message) if add_staged_files(git_repo)
197
+ end
198
+
199
+ # @summary
200
+ # This method when called will call the delete function against the given repository if it exists.
201
+ # @param [String] output_path
202
+ # The repository that is to be deleted.
203
+ def self.clean_env(output_path)
204
+ # If a local copy already exists it is removed
205
+ FileUtils.rm_rf(output_path)
206
+ end
207
+
208
+ # @summary
209
+ # This method when called will clone a given repository into a local location that has also been set.
210
+ # @param [String] namespace
211
+ # The namespace the repository is located in.
212
+ # @param [String] module_name
213
+ # The name of the repository.
214
+ # @param [String] output_path
215
+ # The location the repository is to be cloned to.
216
+ # @return [Git::Base]
217
+ # A git object representing the local repository or true if already exist
218
+ def self.clone_directory(namespace, module_name, output_path)
219
+ # not all urls are public facing so we need to conditionally use the correct separator
220
+ sep = configuration.git_base_uri.start_with?('git@') ? ':' : '/'
221
+ clone_url = "#{configuration.git_base_uri}#{sep}#{namespace}/#{module_name}.git"
222
+ Git.clone(clone_url, output_path.to_s)
223
+ rescue ::Git::GitExecuteError => error
224
+ PdkSync::Logger.fatal "Cloning #{module_name} has failed. #{error}"
225
+ end
226
+
227
+ # @summary
228
+ # This method when called will run a command command at the given location, with an error message being thrown if it is not successful.
229
+ # @param [String] output_path
230
+ # The location that the command is to be run from.
231
+ # @param [String] command
232
+ # The command to be run.
233
+ # @return [Integer]
234
+ # The status code of the command run.
235
+ def self.run_command(output_path, command, option)
236
+ stdout = ''
237
+ stderr = ''
238
+ status = Process::Status
239
+ pid = ''
240
+ Dir.chdir(output_path) unless Dir.pwd == output_path
241
+
242
+ # Environment cleanup required due to Ruby subshells using current Bundler environment
243
+ if option.nil? == true
244
+ if command =~ %r{^bundle}
245
+ Bundler.with_clean_env do
246
+ stdout, stderr, status = Open3.capture3(command)
247
+ end
248
+ else
249
+ stdout, stderr, status = Open3.capture3(command)
250
+ end
251
+ PdkSync::Logger.info "\n#{stdout}\n"
252
+ PdkSync::Logger.crit "Unable to run command '#{command}': #{stderr}" unless status.exitstatus.zero?
253
+ status.exitstatus
254
+ else
255
+ # Environment cleanup required due to Ruby subshells using current Bundler environment
256
+ if command =~ %r{^sh }
257
+ Bundler.with_clean_env do
258
+ pid = spawn(command, out: 'run_command.out', err: 'run_command.err')
259
+ Process.detach(pid)
260
+ end
261
+ end
262
+ pid
263
+ end
264
+ end
265
+
266
+ # @summary
267
+ # This method when called will run the 'pdk update --force' command at the given location, with an error message being thrown if it is not successful.
268
+ # @param [String] output_path
269
+ # The location that the command is to be run from.
270
+ # @return [Integer]
271
+ # The status code of the pdk update run.
272
+ def self.pdk_update(output_path)
273
+ # Runs the pdk update command
274
+ Dir.chdir(output_path) do
275
+ _, module_temp_ref = module_templates_url.split('#')
276
+ module_temp_ref ||= configuration.pdk_templates_ref
277
+ template_ref = configuration.module_is_authoritive ? module_temp_ref : configuration.pdk_templates_ref
278
+ change_module_template_url(configuration.pdk_templates_url, template_ref) unless configuration.module_is_authoritive
279
+ _stdout, stderr, status = Open3.capture3("#{return_pdk_path} update --force --template-ref=#{template_ref}")
280
+ PdkSync::Logger.fatal "Unable to run `pdk update`: #{stderr}" unless status.exitstatus.zero?
281
+ status.exitstatus
282
+ end
283
+ end
284
+
285
+ # @summary
286
+ # This method when called will retrieve the template ref of the current module, i.e. the one that was navigated into in the 'pdk_update' method.
287
+ # @param [String] metadata_file
288
+ # An optional input that can be used to set the location of the metadata file.
289
+ # @return [String]
290
+ # A string value that represents the current pdk template.
291
+ def self.return_template_ref(metadata_file = 'metadata.json')
292
+ file = File.read(metadata_file)
293
+ data_hash = JSON.parse(file)
294
+ data_hash['template-ref']
295
+ end
296
+
297
+ # @summary
298
+ # This method when called will retrieve the tempalate-url of the current module,
299
+ # @param metadata_file [String]
300
+ # An optional input that can be used to set the location of the metadata file.
301
+ # @param url [String] - the url of the pdk-templates repo
302
+ # @return [String]
303
+ # A string value that represents the current pdk tempalate-url.
304
+ def self.module_templates_url(metadata_file = 'metadata.json')
305
+ file = File.read(metadata_file)
306
+ data_hash = JSON.parse(file)
307
+ data_hash['template-url']
308
+ end
309
+
310
+ # @param [String] - the url of the pdk-templates
311
+ # @param [String] - the ref of the pdk templates you want to change to
312
+ # @return [String] - the updated url
313
+ def self.change_module_template_url(url, ref, metadata_file = 'metadata.json')
314
+ content = File.read(metadata_file)
315
+ uri = PDK::Util::TemplateURI.uri_safe(url.to_s + "##{ref}")
316
+ data_hash = JSON.parse(content)
317
+ data_hash['template-url'] = uri
318
+ File.write(metadata_file, data_hash.to_json)
319
+ uri.to_s
320
+ end
321
+
322
+ # @summary
323
+ # This method when called will checkout a new local branch of the given repository.
324
+ # @param [Git::Base] git_repo
325
+ # A git object representing the local repository to be branched.
326
+ # @param [String] branch_suffix
327
+ # The string that is appended on the branch name. eg template_ref or a friendly name
328
+ def self.checkout_branch(git_repo, branch_suffix)
329
+ git_repo.branch("pdksync_#{branch_suffix}").checkout
330
+ end
331
+
332
+ # @summary
333
+ # Check the local pdk version against the most recent tagged release on GitHub
334
+ # @return [Boolean] true if the remote version is less than or equal to local version
335
+ def self.check_pdk_version
336
+ stdout, _stderr, status = Open3.capture3("#{return_pdk_path} --version")
337
+ PdkSync::Logger.fatal "Unable to find pdk at '#{return_pdk_path}'." unless status.exitstatus
338
+ local_version = stdout.strip
339
+ remote_version = Octokit.tags('puppetlabs/pdk').first[:name][1..-1]
340
+ up2date = Gem::Version.new(remote_version) <= Gem::Version.new(local_version)
341
+ unless up2date
342
+ PdkSync::Logger.warn "The current version of pdk is #{remote_version} however you are using #{local_version}"
343
+ end
344
+ up2date
345
+ rescue StandardError => error
346
+ PdkSync::Logger.warn "Unable to check latest pdk version. #{error}"
347
+ end
348
+
349
+ # @summary
350
+ # This method when called will create a directory identified by the set global variable 'configuration.pdksync_dir', on the condition that it does not already exist.
351
+ def self.create_filespace
352
+ FileUtils.mkdir_p configuration.pdksync_dir unless Dir.exist?(configuration.pdksync_dir)
353
+ configuration.pdksync_dir
354
+ end
355
+
356
+ # @summary
357
+ # This method when called will create a directory identified by the set global variable 'configuration.pdksync_gem_dir', on the condition that it does not already exist.
358
+ def self.create_filespace_gem
359
+ FileUtils.mkdir_p configuration.pdksync_gem_dir unless Dir.exist?(configuration.pdksync_gem_dir)
360
+ configuration.pdksync_gem_dir
361
+ end
362
+
363
+ # @summary
364
+ # This method when called will create and return an octokit client with access to the upstream git repositories.
365
+ # @return [PdkSync::GitPlatformClient] client
366
+ # The Git platform client that has been created.
367
+ def self.setup_client
368
+ PdkSync::GitPlatformClient.new(configuration.git_platform, configuration.git_platform_access_settings)
369
+ rescue StandardError => error
370
+ raise "Git platform access not set up correctly: #{error}"
371
+ end
372
+
373
+ # @summary
374
+ # This method when called will create and return an octokit client with access to jenkins.
375
+ # @return [PdkSync::JenkinsClient] client
376
+ # The Git platform client that has been created.
377
+ def self.setup_jenkins_client(jenkins_server_url)
378
+ require 'pdksync/jenkinsclient'
379
+ if configuration.jenkins_platform_access_settings[:jenkins_username].nil?
380
+ raise ArgumentError, "Jenkins access token for #{configuration.jenkins_platform.capitalize} not set"\
381
+ " - use 'export #{configuration.jenkins_platform.upcase}_USERNAME=\"<your username>\"' to set"
382
+ elsif configuration.jenkins_platform_access_settings[:jenkins_password].nil?
383
+ raise ArgumentError, "Jenkins access token for #{jenkins_platform.capitalize} not set"\
384
+ " - use 'export #{jenkins_platform.upcase}_PASSWORD=\"<your password>\"' to set"
385
+ end
386
+ PdkSync::JenkinsClient.new(jenkins_server_url, configuration.jenkins_platform_access_settings)
387
+ rescue StandardError => error
388
+ raise "Jenkins platform access not set up correctly: #{error}"
389
+ end
390
+
391
+ # @summary
392
+ # This method when called will access a file set by the global variable 'configuration.managed_modules' and retrieve the information within as an array.
393
+ # @return [Array]
394
+ # An array of different module names.
395
+ def self.return_modules
396
+ raise "File '#{configuration.managed_modules}' is empty/does not exist" if File.size?(configuration.managed_modules).nil?
397
+ YAML.safe_load(File.open(configuration.managed_modules))
398
+ end
399
+
400
+ # @summary
401
+ # This method when called will parse an array of module names and verify
402
+ # whether they are valid repo or project names on the configured Git
403
+ # hosting platform.
404
+ # @param [PdkSync::GitPlatformClient] client
405
+ # The Git platform client used to get a repository.
406
+ # @param [Array] module_names
407
+ # String array of the names of Git platform repos
408
+ def self.validate_modules_exist(client, module_names)
409
+ raise "Error reading in modules. Check syntax of '#{configuration.managed_modules}'." unless !module_names.nil? && module_names.is_a?(Array)
410
+ invalid = module_names.reject { |name| client.repository?("#{configuration.namespace}/#{name}") }
411
+ # Raise error if any invalid matches were found
412
+ raise "Could not find the following repositories: #{invalid}" unless invalid.empty?
413
+ true
414
+ end
415
+
416
+ # @summary
417
+ # This method when called will update a Gemfile and remove the existing version of gem from the Gemfile.
418
+ # @param [String] output_path
419
+ # The location that the command is to be run from.
420
+ # @param [String] gem_to_test
421
+ # The Gem to test.
422
+ # @param [String] gem_line
423
+ # The gem line to replace
424
+ # @param [String] gem_sha_finder
425
+ # The gem sha to find
426
+ # @param [String] gem_sha_replacer
427
+ # The gem sha to replace
428
+ # @param [String] gem_version_finder
429
+ # The gem version to find
430
+ # @param [String] gem_version_replacer
431
+ # The gem version to replace
432
+ # @param [String] gem_branch_finder
433
+ # The gem branch to find
434
+ # @param [String] gem_branch_replacer
435
+ # The gem branch to replace
436
+ def self.gem_file_update(output_path, gem_to_test, gem_line, gem_sha_finder, gem_sha_replacer, gem_version_finder, gem_version_replacer, gem_branch_finder, gem_branch_replacer, main_path)
437
+ gem_file_name = 'Gemfile'
438
+ validate_gem_update_module(gem_to_test, gem_line, output_path, main_path)
439
+
440
+ if (gem_line.nil? == false) && (gem_sha_replacer != '\"\"')
441
+ new_data = get_source_test_gem(gem_to_test, gem_line)
442
+ new_data.each do |data|
443
+ if data.include?('branch')
444
+ gem_branch_replacer = data.split(' ')[1].strip.chomp('"').delete("'")
445
+ elsif data.include?('ref')
446
+ gem_sha_replacer = data.split(' ')[1].strip.chomp('').delete("'")
447
+ elsif data =~ %r{~>|=|>=|<=|<|>}
448
+ delimiters = ['>', '<', '>=', '<=', '=']
449
+ version_to_check = data.split(Regexp.union(delimiters))[1].chomp('""').delete("'")
450
+ validate_gem_version_replacer(version_to_check.to_s, gem_to_test)
451
+ end
452
+ end
453
+ end
454
+
455
+ if gem_sha_replacer.nil? == false && gem_sha_replacer != '\"\"' && gem_sha_replacer != ''
456
+ validate_gem_sha_replacer(gem_sha_replacer.chomp('"').reverse.chomp('"').reverse, gem_to_test)
457
+ end
458
+ if gem_branch_replacer.nil? == false && gem_branch_replacer != '\"\"'
459
+ validate_gem_branch_replacer(gem_branch_replacer.chomp('"').reverse.chomp('"').reverse, gem_to_test)
460
+ end
461
+ if gem_version_replacer.nil? == false && gem_version_replacer != '\"\"' && gem_version_replacer != ''
462
+ delimiters = ['<', '>', '<=', '>=', '=']
463
+ version_to_check = gem_version_replacer.split(Regexp.union(delimiters))
464
+ version_to_check.each do |version|
465
+ next if version.nil?
466
+ validate_gem_version_replacer(version.to_s, gem_to_test) unless version == ''
467
+ end
468
+ end
469
+
470
+ Dir.chdir(output_path) unless Dir.pwd == output_path
471
+
472
+ line_number = 1
473
+ gem_update_sha = [
474
+ { finder: "ref: '#{gem_sha_finder}'",
475
+ replacer: "ref: '#{gem_sha_replacer}'" }
476
+ ]
477
+ gem_update_version = [
478
+ { finder: gem_version_finder,
479
+ replacer: gem_version_replacer }
480
+ ]
481
+ gem_update_branch = [
482
+ { finder: "branch: '#{gem_branch_finder}'",
483
+ replacer: "branch: '#{gem_branch_replacer}'" }
484
+ ]
485
+ # gem_line option is passed
486
+
487
+ if gem_line.nil? == false && (gem_line != '' || gem_line != '\"\"')
488
+
489
+ # Delete the gem in the Gemfile to add the new line
490
+ gem_test = gem_to_test.chomp('"').reverse.chomp('"').reverse
491
+ File.open(temp_file_path, 'w') do |out_file|
492
+ File.foreach(gem_file_name) do |line|
493
+ out_file.puts line unless line =~ %r{#{gem_test}}
494
+ end
495
+ end
496
+ FileUtils.mv(temp_file_path, gem_file_name)
497
+
498
+ # Insert the new Gem to test
499
+ file = File.open(gem_file_name)
500
+ contents = file.readlines.map(&:chomp)
501
+ contents.insert(line_number, gem_line.chomp('"').reverse.chomp('"').reverse)
502
+ File.open(gem_file_name, 'w') { |f| f.write contents.join("\n") }
503
+ end
504
+
505
+ # gem_sha_finder and gem_sha_replacer options are passed
506
+ if gem_sha_finder.nil? == false && gem_sha_replacer.nil? == false && gem_sha_finder != '' && gem_sha_finder != '\"\"' && gem_sha_replacer != '' && gem_sha_replacer != '\"\"'
507
+ # Replace with SHA
508
+ file = File.open(gem_file_name)
509
+ contents = file.readlines.join
510
+ gem_update_sha.each do |regex|
511
+ contents = contents.gsub(%r{#{regex[:finder]}}, regex[:replacer])
512
+ end
513
+ File.open(gem_file_name, 'w') { |f| f.write contents.to_s }
514
+ end
515
+
516
+ # gem_version_finder and gem_version_replacer options are passed
517
+ if gem_version_finder.nil? == false && gem_version_replacer.nil? == false && gem_version_finder != '' && gem_version_finder != '\"\"' && gem_version_replacer != '' && gem_version_replacer != '\"\"' # rubocop:disable Metrics/LineLength
518
+ # Replace with version
519
+ file = File.open(gem_file_name)
520
+ contents = file.readlines.join
521
+ gem_update_version.each do |regex|
522
+ contents = contents.gsub(%r{#{regex[:finder]}}, regex[:replacer])
523
+ end
524
+ File.open(gem_file_name, 'w') { |f| f.write contents.to_s }
525
+ end
526
+
527
+ # gem_branch_finder and gem_branch_replacer options are passed
528
+ if gem_branch_finder.nil? == false && gem_branch_replacer.nil? == false && gem_branch_finder != '' && gem_branch_finder != '\"\"' && gem_branch_replacer != '' && gem_branch_replacer != '\"\"' # rubocop:disable Metrics/LineLength, Style/GuardClause
529
+ # Replace with branch
530
+ file = File.open(gem_file_name)
531
+ contents = file.readlines.join
532
+ gem_update_branch.each do |regex|
533
+ contents = contents.gsub(%r{#{regex[:finder]}}, regex[:replacer]) # unless contents =~ %r{#{gem_to_test}}
534
+ end
535
+ File.open(gem_file_name, 'w') { |f| f.write contents.to_s }
536
+ end
537
+ end
538
+
539
+ # @summary
540
+ # This method is used to identify the type of module.
541
+ # @param [String] output_path
542
+ # The location that the command is to be run from.
543
+ # @param [String] repo_name
544
+ # The module name to identify the type
545
+ def self.module_type(output_path, repo_name)
546
+ if repo_name.nil? == false
547
+ module_type = if File.exist?("#{output_path}/provision.yaml")
548
+ 'litmus'
549
+ else
550
+ 'traditional'
551
+ end
552
+ end
553
+ module_type
554
+ end
555
+
556
+ # @summary
557
+ # This method when called will run the 'module tests' command at the given location, with an error message being thrown if it is not successful.
558
+ # @param [String] output_path
559
+ # The location that the command is to be run from.
560
+ # @param [String] module_type
561
+ # The module type (litmus or traditional)
562
+ # @param [String] module_name
563
+ # The module name
564
+ # @param [String] puppet collection
565
+ # The puppet collection
566
+ # @return [Integer]
567
+ # The status code of the pdk update run.
568
+ def self.run_tests_locally(output_path, module_type, provision_type, module_name, puppet_collection)
569
+ provision_type = provision_type.chomp('"').reverse.chomp('"').reverse
570
+ status = Process::Status
571
+ # Save the current path
572
+ old_path = Dir.pwd
573
+
574
+ # Create the acceptance scripts
575
+ file = File.open('acc.sh', 'w')
576
+ file.puts '#!/bin/sh'
577
+
578
+ if puppet_collection
579
+ file.puts "export PUPPET_GEM_VERSION='~> #{puppet_collection}'"
580
+ end
581
+ file.puts "rm -rf #{output_path}/Gemfile.lock;rm -rf #{output_path}/.bundle"
582
+ file.puts 'bundle install --path .bundle/gems/ --jobs 4'
583
+ file.puts "bundle exec rake 'litmus:provision_list[#{provision_type}]'"
584
+ file.puts 'bundle exec rake litmus:install_agent'
585
+ file.puts 'bundle exec rake litmus:install_module'
586
+ file.puts 'bundle exec rake litmus:acceptance:parallel'
587
+ file.puts 'bundle exec rake litmus:tear_down'
588
+ file.close
589
+
590
+ # Runs the module tests command
591
+ if module_type == 'litmus'
592
+ run_command(output_path, 'cp ../../acc.sh .', nil)
593
+ Dir.chdir(old_path)
594
+ run_command(output_path, 'chmod 777 acc.sh', nil)
595
+ Dir.chdir(old_path)
596
+ status = run_command(output_path, 'sh acc.sh 2>&1 | tee litmusacceptance.out', 'background')
597
+ if status != 0
598
+ PdkSync::Logger.info "SUCCESS:Kicking of module Acceptance tests to run for the module #{module_name} - SUCCEED.Results will be available in the following path #{output_path}/litmusacceptance.out.Process id is #{status}"
599
+ else
600
+ PdkSync::Logger.fatal "FAILURE:Kicking of module Acceptance tests to run for the module #{module_name} - FAILED.Results will be available in the following path #{output_path}/litmusacceptance.out."
601
+ end
602
+ end
603
+ PdkSync::Logger.warn "(WARNING) Executing testcases locally supports only for litmus'" if module_type != 'litmus'
604
+ end
605
+
606
+ # @summary
607
+ # This method when called will fetch the module tests results.
608
+ # @param [String] output_path
609
+ # The location that the command is to be run from.
610
+ # @param [String] module_type
611
+ # The module type (litmus or traditional)
612
+ # @param [String] module_name
613
+ # The module name
614
+ # @param [String] report_rows
615
+ # The module test results
616
+ # @return [Integer]
617
+ # The status code of the pdk update run.
618
+ def self.fetch_test_results_locally(output_path, module_type, module_name, report_rows)
619
+ # Save the current path
620
+ old_path = Dir.pwd
621
+ if module_type != 'litmus'
622
+ PdkSync::Logger.warn "(WARNING) Fetching test results locally supports only for litmus'"
623
+ end
624
+
625
+ # Run the tests
626
+ Dir.chdir(old_path)
627
+ lines = IO.readlines("#{output_path}/litmusacceptance.out")[-10..-1]
628
+ if lines.find { |e| %r{exit} =~ e } # rubocop:disable Style/ConditionalAssignment
629
+ report_rows << if lines.find { |e| %r{^Failed} =~ e } || lines.find { |e| %r{--trace} =~ e }
630
+ [module_name, 'FAILED', "Results are available in the following path #{output_path}/litmusacceptance.out"]
631
+ else
632
+ [module_name, 'SUCCESS', "Results are available in the following path #{output_path}/litmusacceptance.out"]
633
+ end
634
+ else
635
+ report_rows << if lines.find { |e| %r{^Failed} =~ e } || lines.find { |e| %r{--trace} =~ e } || lines.find { |e| %r{rake aborted} =~ e }
636
+ [module_name, 'FAILED', "Results are available in the following path #{output_path}/litmusacceptance.out"]
637
+ else
638
+ [module_name, 'PROGRESS', "Results will be available in the following path #{output_path}/litmusacceptance.out"]
639
+ end
640
+ end
641
+ return report_rows if module_type == 'litmus'
642
+ end
643
+
644
+ # @summary
645
+ # This method when called will find the source location of the gem to test
646
+ # @param [String] gem_to_test
647
+ # The gem to test
648
+ # @param [String] gem_line
649
+ # TThe line to update in the Gemfile
650
+ # @return [String]
651
+ # The source location of the gem to test
652
+ def self.get_source_test_gem(gem_to_test, gem_line)
653
+ return gem_line.split(',') if gem_line
654
+ return gem_to_test unless gem_to_test
655
+
656
+ gemfile_line = File.readlines('Gemfile').find do |line|
657
+ line.include?(gem_to_test.to_s)
658
+ end
659
+
660
+ return "https://github.com/puppetlabs/#{gem_to_test}" unless gemfile_line
661
+ gemfile_line =~ %r{(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?}
662
+ line.split(',')[1].strip.to_s if line
663
+ end
664
+
665
+ # @summary
666
+ # This method when called will validate the gem_line to update in the Gemfile
667
+ # @param [String] gem_to_test
668
+ # The gem to test
669
+ # @param [String] gem_line
670
+ # The line to update in the Gemfile
671
+ def self.validate_gem_update_module(gem_to_test, gem_line, output_path, main_path)
672
+ gem_to_test = gem_to_test.chomp('"').reverse.chomp('"').reverse
673
+ Dir.chdir(main_path)
674
+ output_path = "#{configuration.pdksync_dir}/#{gem_to_test}"
675
+ clean_env(output_path) if Dir.exist?(output_path)
676
+ print 'delete module directory, '
677
+
678
+ # when gem_line is specified, we need to parse the line and identify all the values
679
+ # - we can have source url or we need to
680
+ # - sha, branch, version
681
+ if gem_line
682
+ git_repo = get_source_test_gem(gem_to_test, gem_line)
683
+ i = 0
684
+ git_repo.each do |item|
685
+ i += 1
686
+ if item =~ %r{((git@|http(s)?:\/\/)([\w\.@]+)(\/|:))([\w,\-,\_]+)\/([\w,\-,\_]+)(.git){0,1}((\/){0,1})}
687
+ git_repo = item.split('git:')[1].strip.delete("'")
688
+ break
689
+ elsif git_repo.size == i
690
+ # git_repo = "https://github.com/puppetlabs#{gem_to_test}"
691
+ sep = configuration.git_base_uri.start_with?('git@') ? ':' : '/'
692
+ git_repo = "#{configuration.git_base_uri}#{sep}#{configuration.namespace}/#{gem_to_test}"
693
+ end
694
+ end
695
+ print 'clone module directory, '
696
+ git_repo = run_command(configuration.pdksync_dir.to_s, "git clone #{git_repo}", nil)
697
+ elsif gem_to_test
698
+ git_repo = clone_directory(configuration.namespace, gem_to_test, output_path.to_s)
699
+ end
700
+
701
+ Dir.chdir(main_path)
702
+ raise "Unable to clone repo for #{gem_to_test}. Check repository's url to be correct!".red if git_repo.nil?
703
+
704
+ @all_versions = ''
705
+ @all_refs = ''
706
+ @all_branches = ''
707
+
708
+ Dir.chdir(output_path)
709
+
710
+ stdout_refs, stderr_refs, status_refs = Open3.capture3('git show-ref -s')
711
+ @all_refs = stdout_refs
712
+ stdout_branches, stderr_branches, status_branches = Open3.capture3('git branch -a')
713
+ @all_branches = stdout_branches
714
+ stdout_versions, stderr_versions, status_versions = Open3.capture3('git tag')
715
+ @all_versions = stdout_versions
716
+
717
+ raise "Couldn't get references due to #{stderr_refs}".red unless status_refs.exitstatus.zero?
718
+ raise "Couldn't get branches due to #{stderr_branches}".red unless status_branches.exitstatus.zero?
719
+ raise "Couldn't get versions due to #{stderr_versions}".red unless status_versions.exitstatus.zero?
720
+ Dir.chdir(main_path)
721
+ end
722
+
723
+ # @summary
724
+ # This method when called will validate the gem_sha_replacer to update in the Gemfile
725
+ # @param [String] gem_to_test
726
+ # The gem to test
727
+ # @param [String] gem_sha_replacer
728
+ # The sha to update in the Gemfile
729
+ def self.validate_gem_sha_replacer(gem_sha_replacer, gem_to_test)
730
+ found = false
731
+ @all_refs.split(' ').each do |sha|
732
+ puts "SHA #{gem_sha_replacer} valid.\n".green if gem_sha_replacer == sha
733
+ found = true if gem_sha_replacer == sha
734
+ end
735
+ raise "Couldn't find sha: #{gem_sha_replacer} in your repository: #{gem_to_test}".red if found == false
736
+ end
737
+
738
+ # @summary
739
+ # This method when called will validate the gem_branch_replacer to update in the Gemfile
740
+ # @param [String] gem_to_test
741
+ # The gem to test
742
+ # @param [String] gem_branch_replacer
743
+ # The branch to update in the Gemfile
744
+ def self.validate_gem_branch_replacer(gem_branch_replacer, gem_to_test)
745
+ raise "Couldn't find branch: #{gem_branch_replacer} in your repository: #{gem_to_test}".red unless @all_branches.include?(gem_branch_replacer)
746
+ puts "Branch #{gem_branch_replacer} valid.\n".green
747
+ end
748
+
749
+ # @summary
750
+ # This method when called will validate the gem_version_replacer to update in the Gemfile
751
+ # @param [String] gem_to_test
752
+ # The gem to test
753
+ # @param [String] gem_version_replacer
754
+ # The version to update in the Gemfile
755
+ def self.validate_gem_version_replacer(gem_version_replacer, gem_to_test)
756
+ found = false
757
+ @all_versions.split(' ').each do |version|
758
+ puts "Version #{gem_version_replacer} valid.\n".green if gem_version_replacer == version
759
+ found = true if gem_version_replacer == version
760
+ end
761
+ raise "Couldn't find version: #{gem_version_replacer} in your repository: #{gem_to_test}".red if found == false
762
+ end
763
+
764
+ # @summary
765
+ # This method when called will create a pr on the given repository that will create a pr to merge the given commit into the main with the pdk version as an identifier.
766
+ # @param [PdkSync::GitPlatformClient] client
767
+ # The Git platform client used to gain access to and manipulate the repository.
768
+ # @param [String] ouput_path
769
+ # The location that the command is to be run from.
770
+ # @param [String] jenkins_client
771
+ # Jenkins authentication.
772
+ # @param [String] repo_name
773
+ # Module to run on Jenkins
774
+ # @param [String] current_branch
775
+ # The branch against which the user needs to run the jenkin jobs
776
+ def self.run_tests_jenkins(jenkins_client, repo_name, current_branch, github_user, job_name)
777
+ if jenkins_client.nil? == false || repo_name.nil? == false || current_branch.nil? == false
778
+ pr = jenkins_client.create_adhoc_job(repo_name,
779
+ current_branch,
780
+ github_user,
781
+ job_name)
782
+ pr
783
+ end
784
+ rescue StandardError => error
785
+ puts "(FAILURE) Jenkins Job creation for #{repo_name} has failed. #{error}".red
786
+ end
787
+
788
+ # convert duration from ms to format h m s ms
789
+ def self.duration_hrs_and_mins(ms)
790
+ return '' unless ms
791
+ hours, ms = ms.divmod(1000 * 60 * 60)
792
+ minutes, ms = ms.divmod(1000 * 60)
793
+ seconds, ms = ms.divmod(1000)
794
+ "#{hours}h #{minutes}m #{seconds}s #{ms}ms"
795
+ end
796
+
797
+ # return jenkins job urls
798
+ def self.adhoc_urls(job_name, jenkins_server_urls)
799
+ adhoc_urls = []
800
+ # get adhoc jobs
801
+ adhoc_urls.push("#{jenkins_server_urls}/job/#{job_name}")
802
+ adhoc_urls.each do |url|
803
+ conn = Faraday::Connection.new "#{url}/api/json"
804
+ res = conn.get
805
+ build_job_data = JSON.parse(res.body.to_s)
806
+ downstream_job = build_job_data['downstreamProjects']
807
+ break if downstream_job.empty?
808
+ downstream_job.each do |item|
809
+ next if item.nil?
810
+ adhoc_urls.push(item['url']) unless item['url'].nil? && item['url'].include?('skippable_adhoc')
811
+ end
812
+ end
813
+ adhoc_urls
814
+ end
815
+
816
+ # test_results_jenkins
817
+ def self.test_results_jenkins(jenkins_server_url, build_id, job_name, module_name)
818
+ PdkSync::Logger.info 'Fetch results from jenkins'
819
+ # remove duplicates and sort the list
820
+ adhoc_urls = adhoc_urls(job_name, jenkins_server_url).uniq.sort_by { |url| JSON.parse(Faraday.get("#{url}/api/json").body.to_s)['fullDisplayName'].scan(%r{[0-9]{2}\s}).first.to_i }
821
+ report_rows = []
822
+ @failed = false
823
+ @in_progress = false
824
+ @aborted = false
825
+
826
+ File.delete("results_#{module_name}.out") if File.exist?("results_#{module_name}.out")
827
+ # analyse each build result - get status, execution time, logs_link
828
+ @data = "MODULE_NAME=#{module_name}\nBUILD_ID=#{build_id}\nINITIAL_job=#{jenkins_server_url}/job/#{job_name}/#{build_id}\n\n"
829
+ write_to_file("results_#{module_name}.out", @data)
830
+ PdkSync::Logger.info "Analyse test execution report \n"
831
+ adhoc_urls.each do |url|
832
+ # next if skipped in build name
833
+ current_build_data = JSON.parse(Faraday.get("#{url}/api/json").body.to_s)
834
+ next if url.include?('skippable_adhoc') || current_build_data['color'] == 'notbuilt'
835
+ next if current_build_data['fullDisplayName'].downcase.include?('skipped')
836
+ returned_data = get_data_build(url, build_id, module_name) unless @failed || @in_progress
837
+ generate_report_table(report_rows, url, returned_data)
838
+ end
839
+
840
+ table = Terminal::Table.new title: "Module Test Results for: #{module_name}\nCheck results in #{Dir.pwd}/results_#{module_name}.out ", headings: %w[Status Result Execution_Time], rows: report_rows
841
+ PdkSync::Logger.info "SUCCESSFUL test results!\n".green unless @failed || @in_progress
842
+ PdkSync::Logger.info "\n#{table} \n"
843
+ end
844
+
845
+ # generate report table when running tests on jenkins
846
+ def self.generate_report_table(report_rows, url, data)
847
+ if @failed
848
+ report_rows << ['FAILED', url, data[1]] unless data.nil?
849
+ elsif @aborted
850
+ report_rows << ['ABORTED', url, data[1]] unless data.nil?
851
+ else
852
+ report_rows << [data[0], url, data[1]] unless data.nil?
853
+ end
854
+ end
855
+
856
+ # for each build from adhoc jobs, get data
857
+ # if multiple platforms under current url, get data for each platform
858
+ def self.get_data_build(url, build_id, module_name)
859
+ current_build_data = JSON.parse(Faraday.get("#{url}/api/json").body.to_s)
860
+ if current_build_data['activeConfigurations'].nil?
861
+ returned_data = analyse_jenkins_report(url, module_name, build_id)
862
+ if returned_data[0] == 'in progress'
863
+ @in_progress = true
864
+ elsif returned_data[0] == 'FAILURE'
865
+ @failed = true
866
+ elsif returned_data[0] == 'ABORTED'
867
+ @aborted = true
868
+ end
869
+ else
870
+ platforms_list = []
871
+ current_build_data['activeConfigurations'].each do |url_child|
872
+ next if url_child['color'] == 'notbuilt'
873
+ platforms_list.push(url_child['url'])
874
+ end
875
+
876
+ platforms_list.each do |platform_build|
877
+ returned_data = analyse_jenkins_report(platform_build, module_name, build_id)
878
+ if returned_data[0] == 'in progress'
879
+ @in_progress = true
880
+ elsif returned_data[0] == 'FAILURE'
881
+ @failed = true
882
+ elsif returned_data[0] == 'ABORTED'
883
+ @aborted = true
884
+ end
885
+ end
886
+ end
887
+
888
+ @data = "\nFAILURE. Fix the failures and rerun tests!\n" if @failed
889
+ @data = "\nIN PROGRESS. Please check test report after the execution is done!\n" if @in_progress
890
+ write_to_file("results_#{module_name}.out", @data) if @failed || @in_progress
891
+ PdkSync::Logger.info 'Failed status! Fix errors and rerun.'.red if @failed
892
+ PdkSync::Logger.info 'Aborted status! Fix errors and rerun.'.red if @aborted
893
+ PdkSync::Logger.info 'Tests are still running! You can fetch the results later by using this task: test_results_jenkins'.blue if @in_progress
894
+ returned_data
895
+ end
896
+
897
+ # write test report to file
898
+ def self.write_to_file(file, _data)
899
+ File.open(file, 'a') do |f|
900
+ f.write @data
901
+ end
902
+ end
903
+
904
+ # analyse jenkins report
905
+ def self.analyse_jenkins_report(url, module_name, build_id)
906
+ # builds don't have the same build_id. That's why just the init build will be identified by id, rest of them by lastBuild
907
+ last_build_job_data = JSON.parse(Faraday.get("#{url}/#{build_id}/api/json").body.to_s) if url.include?('init-manual-parameters_adhoc')
908
+ last_build_job_data = JSON.parse(Faraday.get("#{url}/lastBuild/api/json").body.to_s) unless url.include?('init-manual-parameters_adhoc')
909
+
910
+ # status = 'not_built' unless last_build_job_data
911
+ if last_build_job_data['result'].nil?
912
+ status = 'in progress'
913
+ execution_time = 'running'
914
+ else
915
+ status = last_build_job_data['result']
916
+ execution_time = duration_hrs_and_mins(last_build_job_data['duration'].to_i)
917
+ end
918
+
919
+ # execution_time = 0 unless last_build_job_data
920
+ logs_link = "#{url}/#{build_id}/" if url.include?('init-manual-parameters_adhoc')
921
+ logs_link = "#{url}lastBuild/" unless url.include?('init-manual-parameters_adhoc')
922
+ @data = "Job title =#{last_build_job_data['fullDisplayName']}\n logs_link = #{logs_link}\n status = #{status}\n"
923
+ return_data = [status, execution_time]
924
+ write_to_file("results_#{module_name}.out", @data)
925
+ return_data
926
+ end
927
+
928
+ # @summary
929
+ # Check the most recent tagged release on GitHub for the gem
930
+ # @param [String] gem_to_test
931
+ # The gem to test
932
+ # The current version of the gem
933
+ def self.check_gem_latest_version(gem_to_test)
934
+ remote_version = Octokit.tags("puppetlabs/#{gem_to_test}").first[:name]
935
+ rescue StandardError => error
936
+ puts "(WARNING) Unable to check latest gem version. #{error}".red
937
+ remote_version
938
+ end
939
+
940
+ # @summary
941
+ # Update the gem version by one
942
+ # @param [String] gem_version
943
+ # The current version of the gem
944
+ # The bump version by one of the gem
945
+ def self.update_gem_latest_version_by_one(gem_version)
946
+ current_version = Gem::Version.new gem_version
947
+ new_version = current_version.bump
948
+ rescue StandardError => error
949
+ puts "(WARNING) Unable to check latest gem version. #{error}".red
950
+ new_version
951
+ end
952
+
953
+ # @summary
954
+ # Update Gemfile with multigem
955
+ # @param [String] output_path
956
+ # The location that the command is to be run from.
957
+ # @param [String] gem_name
958
+ # The gem name
959
+ # @param [String] gemfury_token
960
+ # The gemfury token
961
+ # @param [String] gemfury_user
962
+ # The gemfury user
963
+ def self.update_gemfile_multigem(output_path, gem_name, gemfury_token, gemfury_user)
964
+ gem_file_name = 'Gemfile'
965
+ gem_source_line = "source \"https://#{gemfury_token}@gem.fury.io/#{gemfury_user}/\""
966
+ Dir.chdir(output_path) unless Dir.pwd == output_path
967
+
968
+ if gem_name.nil? == false && gemfury_token.nil? == false && gemfury_user.nil? == false # rubocop:disable Style/GuardClause
969
+ # Append the gem with new source location
970
+ gem_name = gem_name.chomp('"').reverse.chomp('"').reverse
971
+ begin
972
+ File.open(temp_file_path, 'w') do |out_file|
973
+ File.foreach(gem_file_name) do |line|
974
+ if line =~ %r{#{gem_name}}
975
+ line = line.chomp
976
+ if line =~ %r{"https://#{gemfury_token}@gem.fury.io/#{gemfury_user}/"}
977
+ puts 'GemFile Already updated'.green
978
+ out_file.puts line.to_s
979
+ else
980
+ out_file.puts "#{line} , :source => \"https://#{gemfury_token}@gem.fury.io/#{gemfury_user}/\""
981
+ end
982
+ else
983
+ out_file.puts line
984
+ end
985
+ end
986
+ end
987
+ FileUtils.mv(temp_file_path, gem_file_name)
988
+
989
+ # Insert the new source Gem location to Gemfile
990
+ file = File.open(gem_file_name)
991
+ contents = file.readlines.map(&:chomp)
992
+ contents.insert(2, gem_source_line) unless contents.include?(gem_source_line)
993
+ File.open(gem_file_name, 'w') { |f| f.write contents.join("\n") }
994
+ rescue Errno::ENOENT => e
995
+ raise "Couldn't find file: #{gem_file_name} #{e} in your repository: #{gem_file_name}".red
996
+ rescue Errno::EACCES => e
997
+ raise "Does not have required permissions to the #{gem_file_name} #{e} in your repository: #{gem_file_name}".red
998
+ end
999
+ end
1000
+ end
1001
+
1002
+ # @summary
1003
+ # Adds an entry to the 'provision.yaml' of a module with the values given
1004
+ # @param [String] module_path
1005
+ # Path to the module root dir
1006
+ # @param [String] key
1007
+ # Key name in 'provision.yaml' (e.g. "release_checks_7)
1008
+ # @param [String] provisioner
1009
+ # The value for the provisioner key (e.g. "abs")
1010
+ # @param [Array] images
1011
+ # The list of images for the images key (e.g. ['ubuntu-1804-x86_64, ubuntu-2004-x86_64', 'centos-8-x86_64'])
1012
+ # @return [Boolean]
1013
+ # True if entry was successfully added to 'provision.yaml'
1014
+ # False if 'provision.yaml' does not exist or is an empty file
1015
+ def self.add_provision_list(module_path, key, provisioner, images)
1016
+ path_to_provision_yaml = "#{module_path}/provision.yaml"
1017
+ return false unless File.exist? path_to_provision_yaml
1018
+ PdkSync::Logger.info "Updating #{path_to_provision_yaml}"
1019
+ provision_yaml = YAML.safe_load(File.read(path_to_provision_yaml))
1020
+ return false if provision_yaml.nil?
1021
+ provision_yaml[key] = {}
1022
+ provision_yaml[key]['provisioner'] = provisioner
1023
+ provision_yaml[key]['images'] = images
1024
+ File.write(path_to_provision_yaml, YAML.dump(provision_yaml))
1025
+ end
1026
+
1027
+ # @summary
1028
+ # Query the 'metadata.json' in the given module path and return the compatible platforms
1029
+ # @param [String] module_path
1030
+ # Path to the module root dir
1031
+ # @return [Hash]
1032
+ # The compatible OSs defined in the 'operatingsystem_support' key of the 'metadata.json'
1033
+ def self.module_supported_platforms(module_path)
1034
+ PdkSync::Logger.info 'Determining supported platforms from metadata.json'
1035
+ os_support_key = 'operatingsystem_support'
1036
+ metadata_json = "#{module_path}/metadata.json"
1037
+ raise 'Could not locate metadata.json' unless File.exist? metadata_json
1038
+ module_metadata = JSON.parse(File.read(metadata_json))
1039
+ raise "Could not locate '#{os_support_key}' key from #{metadata_json}" unless module_metadata.key? os_support_key
1040
+ module_metadata[os_support_key]
1041
+ end
1042
+
1043
+ # @summary
1044
+ # Take a Windows version extracted from the module's 'metadata.json' and normalize it to the version conventions
1045
+ # that VMPooler uses
1046
+ # @param ver
1047
+ # Version from 'metadata.json'
1048
+ # @return [String]
1049
+ # Normalised version that is used by VMPooler templates
1050
+ def self.normalize_win_version(ver)
1051
+ PdkSync::Logger.debug "Normalising Windows version from metadata.json: #{ver}"
1052
+ win_ver_matcher = ver.match(%r{(?:Server\s)?(?<ver>\d+)(?:\s(?<rel>R\d))?})
1053
+ raise "Unable to determine Windows version from metadata.json: #{ver}" unless win_ver_matcher
1054
+ normalized_version = win_ver_matcher['ver']
1055
+ normalized_version += " #{win_ver_matcher['rel'].upcase}" if win_ver_matcher['rel']
1056
+ normalized_version
1057
+ end
1058
+
1059
+ # @summary
1060
+ # Normalize the given os name
1061
+ # @param os
1062
+ # The OS name to normalize
1063
+ # @return [String]
1064
+ # Normalized os name
1065
+ def self.normalize_os(os)
1066
+ case os
1067
+ when %r{aix}i
1068
+ 'AIX'
1069
+ when %r{cent}i
1070
+ 'CentOS'
1071
+ when %r{darwin}i
1072
+ 'Darwin'
1073
+ when %r{deb}i
1074
+ 'Debian'
1075
+ when %r{fedora}i
1076
+ 'Fedora'
1077
+ when %r{oracle}i
1078
+ 'OracleLinux'
1079
+ when %r{osx}i
1080
+ 'OSX'
1081
+ when %r{pan}i
1082
+ 'PAN-OS'
1083
+ when %r{red}i
1084
+ 'RedHat'
1085
+ when %r{sci}i
1086
+ 'Scientific'
1087
+ when %r{suse|sles}i
1088
+ 'SLES'
1089
+ when %r{sol}i
1090
+ 'Solaris'
1091
+ when %r{ubuntu}i
1092
+ 'Ubuntu'
1093
+ when %r{win}i
1094
+ 'Windows'
1095
+ else
1096
+ raise "Could not normalize OS value: #{os}"
1097
+ end
1098
+ end
1099
+
1100
+ # @summary
1101
+ # Get the metadata.json of the given module
1102
+ # @param module_path
1103
+ # Path to the root dir of the module
1104
+ # @return [JSON]
1105
+ # JSON of the metadata.json
1106
+ def self.metadata_json(module_path)
1107
+ metadata_json = "#{module_path}/metadata.json"
1108
+ raise 'Could not locate metadata.json' unless File.exist? metadata_json
1109
+ JSON.parse(File.read(metadata_json))
1110
+ end
1111
+
1112
+ OPERATINGSYSTEM = 'operatingsystem'.freeze
1113
+ OPERATINGSYSTEMRELEASE = 'operatingsystemrelease'.freeze
1114
+ OPERATINGSYSTEM_SUPPORT = 'operatingsystem_support'.freeze
1115
+
1116
+ # @summary
1117
+ # Write the given metadata in JSON format to the given module root dir path
1118
+ # @param module_path
1119
+ # Path to the root dir of the module
1120
+ # @param metadata_json
1121
+ # Metadata in JSON format to write to the module root dir
1122
+ def self.write_metadata_json(module_path, metadata_json)
1123
+ File.open(File.join(module_path, 'metadata.json'), 'w') do |f|
1124
+ f.write(JSON.pretty_generate(metadata_json) + "\n")
1125
+ end
1126
+ end
1127
+
1128
+ # @summary
1129
+ # Normalize the 'operatingsystem_support' entries in the metadata.json
1130
+ # @param module_path
1131
+ # Path to the root dir of the module
1132
+ def self.normalize_metadata_supported_platforms(module_path)
1133
+ new_metadata_json = metadata_json(module_path)
1134
+
1135
+ new_metadata_json[OPERATINGSYSTEM_SUPPORT].each do |os_vers|
1136
+ normalized_os = normalize_os(os_vers[OPERATINGSYSTEM])
1137
+ unless normalized_os == os_vers[OPERATINGSYSTEM]
1138
+ PdkSync::Logger.info "Corrected OS Name: '#{os_vers[OPERATINGSYSTEM]}' -> '#{normalized_os}'"
1139
+ os_vers[OPERATINGSYSTEM] = normalized_os
1140
+ end
1141
+ next unless normalized_os == 'Windows'
1142
+ normalized_vers = os_vers[OPERATINGSYSTEMRELEASE].map { |v| normalize_win_version(v) }
1143
+ unless normalized_vers == os_vers[OPERATINGSYSTEMRELEASE]
1144
+ PdkSync::Logger.info "Corrected OS Versions: #{os_vers[OPERATINGSYSTEMRELEASE]} -> #{normalized_vers}"
1145
+ os_vers[OPERATINGSYSTEMRELEASE] = normalized_vers
1146
+ end
1147
+ end
1148
+
1149
+ write_metadata_json(module_path, new_metadata_json)
1150
+ end
1151
+
1152
+ # @summary
1153
+ # Removes the OS version from the supported platforms
1154
+ # TODO: Remove entire OS entry when version is nil
1155
+ # TODO: Remove entire OS entry when versions is empty
1156
+ # @param module_path
1157
+ # Path to the root dir of the module
1158
+ # @param os_to_remove
1159
+ # OS we want to remove version from
1160
+ # @param version_to_remove
1161
+ # Version from OS we want to remove
1162
+ def self.remove_platform_from_metadata(module_path, os_to_remove, version_to_remove)
1163
+ new_metadata_json = metadata_json(module_path)
1164
+ new_metadata_json[OPERATINGSYSTEM_SUPPORT].each do |os_vers|
1165
+ if (os = normalize_os(os_vers[OPERATINGSYSTEM]))
1166
+ next unless os == os_to_remove
1167
+ vers = os_vers[OPERATINGSYSTEMRELEASE]
1168
+ next unless (ver_index = vers.find_index(version_to_remove))
1169
+ PdkSync::Logger.info "Removing #{os} #{vers[ver_index]} from metadata.json"
1170
+ vers.delete_at(ver_index)
1171
+ else
1172
+ PdkSync::Logger.info 'No entry in metadata.json to replace'
1173
+ return true
1174
+ end
1175
+ end
1176
+ write_metadata_json(module_path, new_metadata_json)
1177
+ end
1178
+
1179
+ # @summary
1180
+ # Adds an OS version to the supported platforms. Creates a new OS entry if it does not exist
1181
+ # @param module_path
1182
+ # Path to the root dir of the module
1183
+ # @param os_to_add
1184
+ # OS we want to add
1185
+ # @param version_to_add
1186
+ # Version we want to add
1187
+ def self.add_platform_to_metadata(module_path, os_to_add, version_to_add)
1188
+ os_to_add = normalize_os(os_to_add)
1189
+ new_metadata_json = metadata_json(module_path)
1190
+ updated_existing_entry = false
1191
+ new_metadata_json[OPERATINGSYSTEM_SUPPORT].each do |os_vers|
1192
+ next unless (os = normalize_os(os_vers[OPERATINGSYSTEM]))
1193
+ next unless os == os_to_add
1194
+ PdkSync::Logger.info "Adding #{os_to_add} version #{version_to_add} to existing entry"
1195
+ os_vers[OPERATINGSYSTEMRELEASE] << version_to_add
1196
+ os_vers[OPERATINGSYSTEMRELEASE].uniq!
1197
+ os_vers[OPERATINGSYSTEMRELEASE].sort_by!(&:to_f)
1198
+ updated_existing_entry = true
1199
+ break
1200
+ end
1201
+ unless updated_existing_entry
1202
+ PdkSync::Logger.info "Adding #{os_to_add} version #{version_to_add} to new entry"
1203
+ supported_platform_entry = {}
1204
+ supported_platform_entry[OPERATINGSYSTEM] = os_to_add
1205
+ supported_platform_entry[OPERATINGSYSTEMRELEASE] = [version_to_add]
1206
+ new_metadata_json[OPERATINGSYSTEM_SUPPORT] << supported_platform_entry
1207
+ end
1208
+ write_metadata_json(module_path, new_metadata_json)
1209
+ end
1210
+
1211
+ NAME = 'name'.freeze
1212
+ REQUIREMENTS = 'requirements'.freeze
1213
+
1214
+ # @summary
1215
+ # Updates the requirements parameter in the metadata.json. If the requirement or a key within it doesn't exist,
1216
+ # it is created.
1217
+ # TODO: Ability to remove requirement
1218
+ # @param module_path
1219
+ # Path to the root dir of the module
1220
+ # @param name
1221
+ # Name attribute of the requirement
1222
+ # @param key
1223
+ # The key name of a K/V pair to be added / updated in the requirement
1224
+ # @param value
1225
+ # The value of the key to be added / updated in the requirement
1226
+ def self.update_requirements(module_path, name, key, value)
1227
+ new_metadata_json = metadata_json(module_path)
1228
+ updated_existing_entry = false
1229
+ new_metadata_json[REQUIREMENTS].each do |requirement|
1230
+ next unless requirement[NAME] == name
1231
+ PdkSync::Logger.info "Updating [#{requirement['name']}] #{requirement.key? key ? "dependency's existing" : 'with a new'} key [#{key}] to value [#{value}]"
1232
+ requirement[key] = value
1233
+ updated_existing_entry = true
1234
+ end
1235
+ unless updated_existing_entry
1236
+ PdkSync::Logger.info "Adding new requirement [#{name}] with key [#{key}] of value [#{value}]"
1237
+ new_requirement = {}
1238
+ new_requirement[NAME] = name
1239
+ new_requirement[key] = value
1240
+ new_metadata_json[REQUIREMENTS] << new_requirement
1241
+ end
1242
+ write_metadata_json(module_path, new_metadata_json)
1243
+ end
1244
+
1245
+ # @summary
1246
+ # Generate an entry in the 'provision.yaml' for running release checks against the platforms that the given
1247
+ # Puppet version. Will compare the supported platforms for the given Puppet version against the compatible
1248
+ # platforms defined in the module's 'metadata.json' and generate a list of platforms that are the same.
1249
+ # @param [String] module_path
1250
+ # Path to the module root dir
1251
+ # @param [String] puppet_version
1252
+ # Puppet version we are generating platform checks for
1253
+ def self.generate_vmpooler_release_checks(module_path, puppet_version)
1254
+ PdkSync::Logger.info "Generating release checks provision.yaml key for Puppet version #{puppet_version}"
1255
+ # This YAML is where the compatible platforms for each Puppet version is stored
1256
+ agent_test_platforms_yaml_file_path = 'lib/pdksync/conf/puppet_abs_supported_platforms.yaml'
1257
+ agent_test_platforms = YAML.safe_load(File.read(agent_test_platforms_yaml_file_path))
1258
+ raise "No configuration for Puppet #{puppet_version} found in #{agent_test_platforms_yaml_file_path}" unless agent_test_platforms.key? puppet_version
1259
+ agent_test_platforms = agent_test_platforms[puppet_version]
1260
+ module_supported_platforms = module_supported_platforms(module_path)
1261
+ images = []
1262
+ PdkSync::Logger.debug 'Processing compatible platforms from metadata.json'
1263
+ module_supported_platforms.each do |os_vers|
1264
+ os = os_vers['operatingsystem'].downcase
1265
+ # 'Windows' and 'OracleLinux' are the definitions in 'metadata.json', however the VMPooler images are 'win' and 'oracle'
1266
+ os = 'win' if os == 'windows'
1267
+ os = 'oracle' if os == 'oraclelinux'
1268
+ vers = os_vers['operatingsystemrelease']
1269
+ if agent_test_platforms.keys.select { |k| k.start_with? os }.empty?
1270
+ PdkSync::Logger.warn "'#{os}' is a compatible platform but was not defined as test platform for Puppet #{puppet_version} in #{agent_test_platforms_yaml_file_path}"
1271
+ next
1272
+ end
1273
+ vers.each do |ver|
1274
+ PdkSync::Logger.debug "Checking '#{os} #{ver}'"
1275
+ if os == 'win'
1276
+ win_ver = normalize_win_version(ver)
1277
+ PdkSync::Logger.debug "Normalised Windows version: #{win_ver}"
1278
+ next unless agent_test_platforms['win'].include? win_ver
1279
+ PdkSync::Logger.debug "'#{os} #{ver}' SUPPORTED by Puppet #{puppet_version}"
1280
+ images << "win-#{win_ver}-x86_64"
1281
+ else
1282
+ next unless agent_test_platforms[os].include? ver
1283
+ PdkSync::Logger.debug "'#{os} #{ver}' SUPPORTED by Puppet #{puppet_version}"
1284
+ images << "#{os}-#{ver.delete('.')}-x86_64"
1285
+ end
1286
+ end
1287
+ end
1288
+ images.uniq!
1289
+ result = add_provision_list(module_path, "release_checks_#{puppet_version}", 'abs', images)
1290
+ PdkSync::Logger.warn "#{module_path}/provision.yaml does not exist" unless result
1291
+ end
1292
+ end
1293
+ end