Skip to content

Commit

Permalink
(maint) matrix from metadata v3
Browse files Browse the repository at this point in the history
  • Loading branch information
h0tw1r3 committed Feb 22, 2024
1 parent e0c7202 commit 08c9cf5
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 12 deletions.
31 changes: 19 additions & 12 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2023-04-18 15:58:23 UTC using RuboCop version 1.48.1.
# on 2024-02-13 06:54:13 UTC using RuboCop version 1.50.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 17
# Offense count: 16
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 116
Max: 105

# Offense count: 8
# Offense count: 10
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
# AllowedMethods: refine
Metrics/BlockLength:
Max: 350
Max: 348

# Offense count: 8
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 33

# Offense count: 20
# Offense count: 18
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Max: 79

# Offense count: 3
# Configuration parameters: CountComments, CountAsOne.
Metrics/ModuleLength:
Max: 324
Max: 255

# Offense count: 7
# Offense count: 8
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 38
Expand Down Expand Up @@ -69,12 +69,12 @@ RSpec/DescribeClass:
- 'spec/exe/matrix_from_metadata_v2_spec.rb'
- 'spec/lib/puppet_litmus/rake_tasks_spec.rb'

# Offense count: 31
# Offense count: 22
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 22

# Offense count: 126
# Offense count: 106
# Configuration parameters: .
# SupportedStyles: have_received, receive
RSpec/MessageSpies:
Expand All @@ -96,15 +96,22 @@ RSpec/NoExpectationExample:
Exclude:
- 'spec/lib/puppet_litmus/rake_helper_spec.rb'

# Offense count: 113
# Offense count: 93
RSpec/StubbedMock:
Exclude:
- 'spec/lib/puppet_litmus/puppet_helpers_spec.rb'
- 'spec/lib/puppet_litmus/rake_helper_spec.rb'
- 'spec/lib/puppet_litmus/rake_tasks_spec.rb'

# Offense count: 6
# Offense count: 7
Style/OpenStructUse:
Exclude:
- 'exe/matrix_from_metadata_v3'
- 'lib/puppet_litmus/puppet_helpers.rb'
- 'spec/spec_helper.rb'

# Offense count: 4
# This cop supports safe autocorrection (--autocorrect).
Style/StderrPuts:
Exclude:
- 'exe/matrix_from_metadata_v3'
10 changes: 10 additions & 0 deletions exe/collections.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"puppet": 7.24,
"ruby": 2.7
},
{
"puppet": 8.0,
"ruby": 3.2
}
]
270 changes: 270 additions & 0 deletions exe/matrix_from_metadata_v3
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'optparse'
require 'ostruct'
require 'json'
require 'English'

# wrap up running in a Github Action
module Action
class << self
def init(to = 'auto')
@notice = true
@type = if to.eql? 'auto'
ENV['GITHUB_ACTIONS'] ? 'github' : 'stdout'
else
to
end
$stderr = $stdout if @type == 'github'
end

def config(**args)
error("invalid Action.config: #{args}") unless args.is_a?(Hash)
args.each do |arg|
instance_variable_set(:"@#{arg[0]}", arg[1])
end
end

def debug(msg)
output(msg, '::debug::') if @debug
end

def notice(msg)
output(msg, '::notice::') if @notice
end

def error(msg)
output(msg, '::error::')
exit 1
end

def warning(msg)
output(msg, '::warning::')
end

def group(message, data)
$stderr.puts "::group::#{message}"
$stderr.puts JSON.pretty_generate(data)
$stderr.puts '::endgroup::'
end

def set_output(key, value)
@output ||= @type == 'github' ? ENV.fetch('GITHUB_OUTPUT', nil) : '/dev/stdout'
error('GITHUB_OUTPUT environment is not set?') if @output.nil?

File.open(@output, 'a') { |f| f.puts("#{key}=#{JSON.generate(value)}") }
end

private

def output(msg, prefix = nil)
$stderr.puts "#{prefix}#{msg}"
end
end
end

options = OpenStruct.new(
puppet_exclude: [],
puppet_include: [],
platform_exclude: [],
platform_include: [],
arch_include: [],
arch_exclude: [],
provision_prefer: [],
provision_include: [],
provision_exclude: []
)

begin
Action.init

ARGV.unshift('--provision-prefer', 'docker') unless ARGV.include? '--provision-prefer'
ARGV.unshift('--metadata', 'metadata.json') unless ARGV.include? '--metadata'
ARGV.unshift('--output', 'auto') unless ARGV.include? '--output'
ARGV.unshift('--collections', File.join(__dir__, 'collections.json')) unless ARGV.include? '--collections'
ARGV.unshift('--provisioners', File.join(__dir__, 'provisioners.json')) unless ARGV.include? '--provisioners'

OptionParser.accept(JSON) do |v|
begin
x = JSON.parse(File.read(v)) if v
raise "nothing parsed from file #{v}" if x.empty?

x
rescue JSON::ParserError
raise "error parsing file #{v}"
end
rescue RuntimeError, Errno::ENOENT
raise OptionParser::InvalidArgument, $ERROR_INFO
end

OutputType = ->(value) {}
OptionParser.accept(OutputType) do |v|
raise OptionParser::InvalidArgument, v \
unless %w[auto github stdout].include?(v)

Action.init(v)
end

ArchType = ->(value) {}
OptionParser.accept(ArchType) do |v|
raise OptionParser::InvalidArgument, v \
unless %w[x86_64 arm64].include?(v)

v
end

OptionParser.new do |opt|
opt.separator "Generate Github Actions Matrices from Puppet metadata.json\n\nOptions:"
opt.on('--puppet-include MAJOR', Integer, 'Select puppet major version') { |o| options.puppet_include << o }
opt.on('--puppet-exclude MAJOR', Integer, 'Filter puppet major version') { |o| options.puppet_exclude << o }
# TODO
# opt.on('--platform-include MATCH', String, 'Select matching platforms') { |o| options.platform_include << o }
# opt.on('--platform-exclude MATCH', String, 'Filter matching platforms') { |o| options.platform_exclude << o }
opt.on('--arch-include ARCH', ArchType, 'Select architecture') { |o| options.arch_include << o }
opt.on('--arch-exclude ARCH', ArchType, 'Filter architecture') { |o| options.arch_exclude << o }
opt.on('--provision-prefer NAME', String, 'Prefer provisioner (default: docker)') { |o| options.provision_prefer << o }
opt.on('--provision-include NAME', String, 'Select provisioner (default: all)') { |o| options.provision_include << o }
opt.on('--provision-exclude NAME', String, 'Filter provisioner') { |o| options.provision_exclude << o }
opt.on('--collections FILE', JSON, 'File containing puppet to ruby version map (default: built-in)') { |o| options.collections = o }
opt.on('--provisioners FILE', JSON, 'File containing platforms by provisioner (default: built-in)') { |o| options.provisioners = o }
opt.on('--metadata FILE', JSON, 'File containing module metadata json (default: metadata.json)') { |o| options.metadata = o }
opt.on('--debug', TrueClass, 'Enable debug messages') { |o| options.debug = o }
opt.on('--quiet', TrueClass, 'Disable notice messages') { |o| options.quiet = o }
opt.on('--output TYPE', OutputType, 'Type of output to generate; auto, github or stdout (default: auto)') { |o| options.metadata = o }
end.parse!

Action.config(debug: true) if options[:debug]
Action.config(notice: false) if options[:quiet]

# validate provisioners
options[:provision_include].select! do |p|
options[:provisioners].key?(p) or raise OptionParser::InvalidArgument, "--provision-include '#{p}' not found in provisioners"
end

# filter provisioners
unless options[:provision_include].empty?
options[:provisioners].delete_if do |k, _|
!options[:provision_include].include?(k.to_s) and Action.notice("provision include filtered #{k}")
end
end
options[:provisioners].delete_if do |k, _|
if options[:provision_exclude].include?(k.to_s)
Action.notice("provision exclude filtered #{k}")
true
end
end

# sort provisioners
options[:provisioners] = options[:provisioners].sort_by { |key, _| options[:provision_prefer].index(key.to_s) || options[:provision_prefer].length }.to_h \
unless options[:provision_prefer].empty?

raise OptionParser::ParseError, 'no provisioners left after filters applied' if options[:provisioners].empty?
rescue OptionParser::ParseError => e
Action.error(e.message)
end

matrix = { platforms: [], collection: [] }
spec_matrix = { include: [] }

# collection matrix
version_re = /([>=<]{1,2})\s*([\d.]+)/
options[:metadata]['requirements']&.each do |req|
next unless req['name'] == 'puppet' && req['version_requirement']

puppet_version_reqs = req['version_requirement'].scan(version_re).map(&:join)
if puppet_version_reqs.empty?
Action.warning("Didn't recognize version_requirement '#{req['version_requirement']}'")
break
end

options[:collections].each do |collection|
next unless options[:puppet_include].each do |major|
break if major != collection['puppet'].to_i

Action.debug("including collection #{collection.inspect}")
end

next unless options[:puppet_exclude].each do |major|
if major.eql? collection['puppet'].to_i
Action.debug("excluding collection #{collection.inspect}")
break
end
end

# Test against the "largest" puppet version in a collection, e.g. `7.9999` to allow puppet requirements with a non-zero lower bound on minor/patch versions.
# This assumes that such a boundary will always allow the latest actually existing puppet version of a release stream, trading off simplicity vs accuracy here.
gem_req = Gem::Requirement.create(puppet_version_reqs)
next unless gem_req.satisfied_by?(Gem::Version.new("#{collection['puppet'].to_i}.9999"))

matrix[:collection] << "puppet#{collection['puppet'].to_i}-nightly"

spec_matrix[:include] << {
puppet_version: "~> #{collection['puppet']}",
ruby_version: collection['ruby']
}
end
end

# Set platforms based on declared operating system support
options[:metadata]['operatingsystem_support'].each do |os_sup|
os_sup['operatingsystemrelease'].sort_by(&:to_i).each do |os_ver|
os_ver_platforms = []
image_key = [os_sup['operatingsystem'], os_ver]

# next unless options[:platform_exclude].each do |match|
# break if major != collection['puppet'].to_i

# Action.debug("including collection #{collection.inspect}")
# end

options[:provisioners].each do |provisioner, platforms|
images = platforms.dig(*image_key)
next if images.nil?

unless options[:arch_include].empty?
images.delete_if do |arch, _|
unless options[:arch_include].include?(arch)
Action.notice("arch include filtered #{image_key.join('-')}-#{arch} from #{provisioner}")
true
end
end
end

images.delete_if do |arch, _|
if options[:arch_exclude].include?(arch)
Action.notice("arch exclude filtered #{image_key.join('-')}-#{arch} from #{provisioner}")
true
end
end

next if images.empty?

images.each do |arch, image|
label = (arch.eql?('x86_64') ? image_key : image_key + [arch]).join('-')
next if os_ver_platforms.any? { |h| h[:label] == label }

os_ver_platforms << {
label: label,
provider: provisioner,
arch: arch,
image: image
}
end
end

if os_ver_platforms.empty?
Action.warning("#{image_key.join('-')} no provisioner found")
else
matrix[:platforms].push(*os_ver_platforms)
end
end
end

Action.group('matrix', matrix) if options[:debug]
Action.group('spec_matrix', spec_matrix) if options[:debug]

Action.error('no collections after filters applied') if matrix[:collection].empty?

Action.set_output('matrix', matrix)
Action.set_output('spec_matrix', spec_matrix)
Loading

0 comments on commit 08c9cf5

Please sign in to comment.