Refactoring to make more testable and composable

This commit is contained in:
Charles Care
2019-08-14 20:58:30 +01:00
parent 751e2dda8c
commit e54d1ca26c
10 changed files with 316 additions and 122 deletions

View File

@@ -21,7 +21,12 @@ end
require "pipeline/analyzer_repo"
require "pipeline/analyzer_build"
require "pipeline/validation/check_invokable"
require "pipeline/validation/check_environment_invariants"
require "pipeline/validation/check_fixtures"
require "pipeline/validate_build"
require "pipeline/util/container_driver"
require "pipeline/util/runc_configurator"
require "pipeline/util/img_wrapper"
require "pipeline/util/runc_wrapper"
require "pipeline/build_image"

View File

@@ -0,0 +1,49 @@
module Pipeline::Util
class ContainerDriver
attr_accessor :runc, :img, :configurator, :workdir
def initialize(runc, img, configurator, workdir)
@runc = runc
@img = img
@configurator = configurator
@workdir = workdir
end
def prepare_workdir
FileUtils.mkdir_p workdir
FileUtils.mkdir_p "#{workdir}/iteration"
FileUtils.mkdir_p "#{workdir}/tmp"
end
def unpack_image(build_tag)
Dir.chdir(workdir) do
img.unpack(build_tag)
end
configurator.setup_for_terminal_access
File.write("#{workdir}/terminal_config.json", configurator.build.to_json)
end
def run_script(script_contents)
File.write("#{workdir}/iteration/test_script.sh", script_contents)
configurator.setup_script("/mnt/exercism-iteration/test_script.sh")
File.write("#{workdir}/test_config.json", configurator.build.to_json)
FileUtils.symlink("#{workdir}/test_config.json", "#{workdir}/config.json", force: true)
run_analyzer
end
def run_analyzer
runc.run(workdir)
end
def run_analyzer_for(exercise_slug)
configurator.invoke_analyzer_for(exercise_slug)
File.write("#{workdir}/analyzer_config.json", configurator.build.to_json)
FileUtils.symlink("#{workdir}/analyzer_config.json", "#{workdir}/config.json", force: true)
run_analyzer
end
end
end

View File

@@ -1,10 +1,11 @@
module Pipeline::Util
class ImgWrapper
attr_accessor :binary_path, :state_location
attr_accessor :binary_path, :state_location, :suppress_output
def initialize
@binary_path = File.expand_path "./opt/img"
@state_location = "/tmp/state-img"
@suppress_output = false
end
def build(local_tag)
@@ -12,21 +13,29 @@ module Pipeline::Util
exec_cmd cmd
end
def unpack(local_tag)
exec_cmd "#{binary_path} unpack -state #{state_location} #{local_tag}"
end
def push_cmd
"#{binary_path} push -state /tmp/state-img"
"#{binary_path} push -state #{state_location}"
end
def push_cmd
"#{binary_path} push -state #{state_location}"
end
def build_cmd
"#{binary_path} build -state /tmp/state-img"
"#{binary_path} build -state #{state_location}"
end
def tag_cmd
"#{binary_path} tag -state /tmp/state-img"
"#{binary_path} tag -state #{state_location}"
end
def exec_cmd(cmd)
puts "> #{cmd}"
puts "------------------------------------------------------------"
puts "> #{cmd}" unless suppress_output
puts "------------------------------------------------------------" unless suppress_output
success = system({}, cmd)
raise "Failed #{cmd}" unless success
end

View File

@@ -0,0 +1,29 @@
module Pipeline::Util
class RuncWrapper
attr_accessor :binary_path, :suppress_output
def initialize
@binary_path = File.expand_path "./opt/runc"
@suppress_output = false
end
def run(container_folder)
Dir.chdir(container_folder) do
exec_cmd run_cmd
end
end
def run_cmd
"#{binary_path} --root root-state run analyzer-#{Time.now.to_i}"
end
def exec_cmd(cmd)
puts "> #{cmd}" unless suppress_output
puts "------------------------------------------------------------" unless suppress_output
success = system({}, cmd)
raise "Failed #{cmd}" unless success
end
end
end

View File

@@ -1,113 +1,30 @@
class Pipeline::ValidateBuild
include Mandate
attr_accessor :img, :runc, :configurator
initialize_with :track_slug, :build_tag
def call
@img = File.expand_path "./opt/img"
@runc = File.expand_path "./opt/runc"
FileUtils.mkdir_p workdir
unpack
write_runc_config
FileUtils.mkdir_p "#{workdir}/iteration"
FileUtils.mkdir_p "#{workdir}/tmp"
check_environment_is_invokable
check_environment_invariants
check_sample_solutions
end
def unpack
Dir.chdir(workdir) do
exec_cmd "#{img} unpack -state /tmp/state-img #{build_tag}"
end
container_driver.prepare_workdir
container_driver.unpack_image(build_tag)
end
def check_environment_is_invokable
run_script <<-EOS
echo "OK" >/mnt/exercism-iteration/result.txt
EOS
text = File.read("#{workdir}/iteration/result.txt")
raise "Santity check failed" if (text.strip != "OK")
Pipeline::Validation::CheckInvokable.(container_driver)
end
def check_environment_invariants
run_script <<-EOS
if ping -q -c 1 -W 1 8.8.8.8 >/mnt/exercism-iteration/ping_check.txt; then
echo "IPv4 is up" >/mnt/exercism-iteration/ip_connectivity.txt
else
echo "IPv4 is down" >/mnt/exercism-iteration/ip_connectivity.txt
fi
EOS
ip_connectivity = File.read("#{workdir}/iteration/ip_connectivity.txt")
raise "IP down check failed" if (ip_connectivity.strip != "IPv4 is down")
run_script <<-EOS
nc -w 3 -v 8.8.8.8 53 2>/mnt/exercism-iteration/net_check.txt
EOS
net_check = File.read("#{workdir}/iteration/net_check.txt")
raise "NET check failed" if (net_check.strip != "8.8.8.8 (8.8.8.8:53) open")
run_script <<-EOS
touch /mnt/exercism-iteration/fs_check.txt
touch /mnt/foobar && echo "/mnt writable!" >>/mnt/exercism-iteration/fs_check.txt
touch /opt/foobar && echo "/opt writable!" >>/mnt/exercism-iteration/fs_check.txt
touch /opt/analyzer/foobar && echo "/opt/analyzer/ writable!" >>/mnt/exercism-iteration/fs_check.txt
exit 0
EOS
fs_check = File.read("#{workdir}/iteration/fs_check.txt")
raise "Not readonly: #{fs_check}" unless fs_check.strip.empty?
run_script <<-EOS
echo "/tmp/ writable!" >/tmp/foobar
EOS
run_script <<-EOS
cat /tmp/foobar >/mnt/exercism-iteration/tmp_check.txt
EOS
tmp_check = File.read("#{workdir}/iteration/tmp_check.txt").strip
tmp_file = File.read("#{workdir}/tmp/foobar").strip
raise "/tmp not persisting across invocations" if ("/tmp/ writable!" != tmp_check || "/tmp/ writable!" != tmp_file)
Pipeline::Validation::CheckEnvironmentInvariants.(container_driver)
end
def check_sample_solutions
exercise_folders = Dir.glob("fixtures/#{track_slug}/*")
exercise_folders.each do |exercise_folder|
exercise_slug = exercise_folder.split("/").last
Dir.glob("#{exercise_folder}/*").each do |fixture_folder|
validate_status(exercise_slug, fixture_folder)
end
end
end
def validate_status(exercise, fixture_folder)
FileUtils.rm_rf("#{workdir}/iteration/")
FileUtils.cp_r "#{fixture_folder}/iteration", "#{workdir}/iteration"
configurator.invoke_analyzer_for(exercise)
File.write("#{workdir}/analyzer_config.json", configurator.build.to_json)
FileUtils.symlink("#{workdir}/analyzer_config.json", "#{workdir}/config.json", force: true)
Dir.chdir(workdir) do
exec_cmd "#{runc} --root root-state run 123"
end
analysis = JSON.parse(File.read("#{workdir}/iteration/analysis.json"))
expected = JSON.parse(File.read("#{fixture_folder}/expected_analysis.json"))
raise "Incorrect expected_status" if expected["status"].nil?
raise "Incorrect status when validating #{fixture_folder}" if expected["status"] != analysis["status"]
expected["comments"] ||= []
analysis["comments"] ||= []
raise "Incorrect comments when validating #{fixture_folder}" if expected["comments"].sort != analysis["comments"].sort
Pipeline::Validation::CheckFixtures.(container_driver, track_slug)
end
memoize
@@ -115,35 +32,13 @@ class Pipeline::ValidateBuild
"/tmp/analyzer-scratch/#{SecureRandom.uuid}"
end
def rootfs
"#{workdir}/rootfs"
end
def run_script(script_contents)
File.write("#{workdir}/iteration/test_script.sh", script_contents)
FileUtils.symlink("#{workdir}/test_config.json", "#{workdir}/config.json", force: true)
Dir.chdir(workdir) do
exec_cmd "#{runc} --root root-state run 123"
end
end
def exec_cmd(cmd)
puts "> #{cmd}"
puts "------------------------------------------------------------"
success = system({}, cmd)
raise "Failed #{cmd}" unless success
end
def write_runc_config
@configurator = Pipeline::Util::RuncConfigurator.new
memoize
def container_driver
img = Pipeline::Util::ImgWrapper.new
runc = Pipeline::Util::RuncWrapper.new
configurator = Pipeline::Util::RuncConfigurator.new
configurator.seed_from_env
configurator.setup_for_terminal_access
File.write("#{workdir}/terminal_config.json", configurator.build.to_json)
configurator.setup_script("/mnt/exercism-iteration/test_script.sh")
File.write("#{workdir}/test_config.json", configurator.build.to_json)
Pipeline::Util::ContainerDriver.new(runc, img, configurator, workdir)
end
end

View File

@@ -0,0 +1,59 @@
module Pipeline::Validation
class CheckEnvironmentInvariants
include Mandate
initialize_with :container_driver
def call
run_script <<-EOS
if ping -q -c 1 -W 1 8.8.8.8 >/mnt/exercism-iteration/ping_check.txt; then
echo "IPv4 is up" >/mnt/exercism-iteration/ip_connectivity.txt
else
echo "IPv4 is down" >/mnt/exercism-iteration/ip_connectivity.txt
fi
EOS
ip_connectivity = File.read("#{workdir}/iteration/ip_connectivity.txt")
raise "IP down check failed" if (ip_connectivity.strip != "IPv4 is down")
run_script <<-EOS
nc -w 3 -v 8.8.8.8 53 2>/mnt/exercism-iteration/net_check.txt
EOS
net_check = File.read("#{workdir}/iteration/net_check.txt")
raise "NET check failed" if (net_check.strip != "8.8.8.8 (8.8.8.8:53) open")
run_script <<-EOS
touch /mnt/exercism-iteration/fs_check.txt
touch /mnt/foobar && echo "/mnt writable!" >>/mnt/exercism-iteration/fs_check.txt
touch /opt/foobar && echo "/opt writable!" >>/mnt/exercism-iteration/fs_check.txt
touch /opt/analyzer/foobar && echo "/opt/analyzer/ writable!" >>/mnt/exercism-iteration/fs_check.txt
exit 0
EOS
fs_check = File.read("#{workdir}/iteration/fs_check.txt")
raise "Not readonly: #{fs_check}" unless fs_check.strip.empty?
run_script <<-EOS
echo "/tmp/ writable!" >/tmp/foobar
EOS
run_script <<-EOS
cat /tmp/foobar >/mnt/exercism-iteration/tmp_check.txt
EOS
tmp_check = File.read("#{workdir}/iteration/tmp_check.txt").strip
tmp_file = File.read("#{workdir}/tmp/foobar").strip
raise "/tmp not persisting across invocations" if ("/tmp/ writable!" != tmp_check || "/tmp/ writable!" != tmp_file)
end
def run_script(script_contents)
container_driver.run_script(script_contents)
end
def workdir
container_driver.workdir
end
end
end

View File

@@ -0,0 +1,41 @@
module Pipeline::Validation
class CheckFixtures
include Mandate
initialize_with :container_driver, :track_slug
def call
clean_and_setup
exercise_folders = Dir.glob("fixtures/#{track_slug}/*")
exercise_folders.each do |exercise_folder|
exercise_slug = exercise_folder.split("/").last
Dir.glob("#{exercise_folder}/*").each do |fixture_folder|
validate_status(exercise_slug, fixture_folder)
end
end
end
def clean_and_setup
FileUtils.rm_rf("#{workdir}/iteration/")
end
def validate_status(exercise, fixture_folder)
FileUtils.cp_r "#{fixture_folder}/iteration", "#{workdir}/iteration"
container_driver.run_analyzer_for(exercise)
analysis = JSON.parse(File.read("#{workdir}/iteration/analysis.json"))
expected = JSON.parse(File.read("#{fixture_folder}/expected_analysis.json"))
raise "Incorrect expected_status" if expected["status"].nil?
raise "Incorrect status when validating #{fixture_folder}" if expected["status"] != analysis["status"]
expected["comments"] ||= []
analysis["comments"] ||= []
raise "Incorrect comments when validating #{fixture_folder}" if expected["comments"].sort != analysis["comments"].sort
end
def workdir
container_driver.workdir
end
end
end

View File

@@ -0,0 +1,21 @@
module Pipeline::Validation
class CheckInvokable
include Mandate
initialize_with :container_driver
def call
container_driver.run_script <<-EOS
echo "OK" >/mnt/exercism-iteration/result.txt
EOS
text = File.read("#{workdir}/iteration/result.txt")
raise "Santity check failed" if (text.strip != "OK")
end
def workdir
container_driver.workdir
end
end
end