dmatrix 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b884e0670f6ecedebb47a88ecb56dd6f790ae672580493bb6f3a853cda71938
4
+ data.tar.gz: ae3960ae3408be5ddaf75bf670be80e4eb3f1daea63c2f7512f2baf249335b4e
5
+ SHA512:
6
+ metadata.gz: 3b8b586ff6af822e4a320838c4be798aa3ff5932571ef6c26b56e6f6e2b1e264e020c7bdb01ad95aec52b8af9f3c15aa055baaaf5d2c752fc5973a60cae24b0f
7
+ data.tar.gz: f58d296ea65ffad4163683975b3a82b3021f046f5e1a9999b40a287f624dddab229d111f74147f3a07ebdae8d0476e864a7ce3928ebb50955160dcc000472bcb
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ spec/examples.txt
@@ -0,0 +1,5 @@
1
+ matrix:
2
+ build_arg:
3
+ FROM_IMAGE:
4
+ - ruby:2.5-alpine
5
+ - ruby:2.6-alpine
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0
4
+
5
+ - Initial release
@@ -0,0 +1,2 @@
1
+ ARG FROM_IMAGE=ruby:2.6-alpine
2
+ FROM $FROM_IMAGE
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dmatrix.gemspec
4
+ gemspec
@@ -0,0 +1,47 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dmatrix (0.1.0)
5
+ parallel (~> 1.13)
6
+ slop (~> 4.5)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ coderay (1.1.2)
12
+ diff-lcs (1.3)
13
+ jet_black (0.5.1)
14
+ method_source (0.9.2)
15
+ parallel (1.17.0)
16
+ pry (0.12.2)
17
+ coderay (~> 1.1.0)
18
+ method_source (~> 0.9.0)
19
+ rake (12.3.2)
20
+ rspec (3.8.0)
21
+ rspec-core (~> 3.8.0)
22
+ rspec-expectations (~> 3.8.0)
23
+ rspec-mocks (~> 3.8.0)
24
+ rspec-core (3.8.0)
25
+ rspec-support (~> 3.8.0)
26
+ rspec-expectations (3.8.3)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.8.0)
29
+ rspec-mocks (3.8.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.8.0)
32
+ rspec-support (3.8.0)
33
+ slop (4.6.2)
34
+
35
+ PLATFORMS
36
+ ruby
37
+
38
+ DEPENDENCIES
39
+ bundler
40
+ dmatrix!
41
+ jet_black (~> 0.5)
42
+ pry
43
+ rake
44
+ rspec (~> 3.8)
45
+
46
+ BUNDLED WITH
47
+ 2.0.1
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Oliver Peate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,93 @@
1
+ # Dmatrix
2
+
3
+ Docker matrix. Parallel execution of each possible combination defined in your
4
+ matrix file.
5
+
6
+ The matrix file can contain `build_arg` and `env` values which will be passed
7
+ to Docker during the image build and execution of your command.
8
+
9
+ Inspired by the [Travis Build Matrix](https://docs.travis-ci.com/user/build-matrix/).
10
+
11
+ ## Example
12
+
13
+ ### `.matrix.yaml`
14
+
15
+ ```
16
+ matrix:
17
+ build_arg:
18
+ FROM_IMAGE:
19
+ - ruby:2.4-alpine
20
+ - ruby:2.5-alpine
21
+ - ruby:2.6-alpine
22
+ BUNDLE_GEMFILE:
23
+ - gemfiles/factory_bot_4_8.gemfile
24
+ - gemfiles/factory_bot_5.gemfile
25
+ ```
26
+
27
+ This would produce six combinations. In this example the `gemfiles` are created
28
+ with the [Appraisal gem](https://github.com/thoughtbot/appraisal).
29
+
30
+ ### `Dockerfile`
31
+
32
+ ```
33
+ ARG FROM_IMAGE=ruby:2.6-alpine
34
+ FROM $FROM_IMAGE
35
+
36
+ RUN apk update && \
37
+ apk add git && \
38
+ mkdir -p /app/lib/my_gem
39
+
40
+ WORKDIR /app
41
+
42
+ COPY Gemfile* my_gem.gemspec Appraisals /app/
43
+ COPY gemfiles/*.gemfile /app/gemfiles/
44
+ COPY lib/my_gem/version.rb /app/lib/my_gem/version.rb
45
+
46
+ ARG BUNDLE_GEMFILE=Gemfile
47
+ ENV BUNDLE_GEMFILE=$BUNDLE_GEMFILE
48
+
49
+ RUN bundle install --jobs=4 --retry=3
50
+
51
+ COPY . /app
52
+ ```
53
+
54
+ ### Running a command
55
+
56
+ ```
57
+ bundle exec dmatrix -- bundle exec rspec
58
+ ```
59
+
60
+ Would use `dmatrix` to build & run `bundle exec rspec` in each combination your
61
+ matrix defines.
62
+
63
+ ## Setup
64
+
65
+ ```
66
+ # Gemfile
67
+
68
+ gem "dmatrix"
69
+ ```
70
+
71
+ Create a `.matrix.yaml` file:
72
+
73
+ ```
74
+ matrix:
75
+ build_arg:
76
+ ARG1:
77
+ - abc
78
+ - def
79
+ env:
80
+ ENV1:
81
+ - 123
82
+ - 456
83
+ ```
84
+
85
+ N.B. the `build_arg` and `env` keys can both contain multiple variants.
86
+
87
+ Run:
88
+
89
+ ```
90
+ bundle exec dmatrix -- <your command>
91
+ ```
92
+
93
+ If not command is specified the default Docker command should run.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dmatrix"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "dmatrix/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dmatrix"
8
+ spec.version = Dmatrix::VERSION
9
+ spec.authors = ["Oliver Peate"]
10
+
11
+ spec.summary = "Docker matrix runner with parallel execution"
12
+ spec.homepage = "https://github.com/odlp/dmatrix"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
16
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ end
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "parallel", "~> 1.13"
23
+ spec.add_dependency "slop", "~> 4.5"
24
+
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "jet_black", "~> 0.5"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rspec", "~> 3.8"
30
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "slop"
4
+ require "dmatrix"
5
+
6
+ opts = Slop.parse do |o|
7
+ o.string "-m", "--matrix", "Matrix file", default: ".matrix.yaml"
8
+ o.string "--log-dir", "Log directory", default: "tmp/dmatrix"
9
+ o.on "--version", "Display the version" do
10
+ puts Dmatrix::VERSION
11
+ exit 0
12
+ end
13
+ end
14
+
15
+ Dmatrix::Runner.new(options: opts.to_hash, run_command: opts.arguments).call
@@ -0,0 +1,5 @@
1
+ require "dmatrix/version"
2
+ require "dmatrix/runner"
3
+
4
+ module Dmatrix
5
+ end
@@ -0,0 +1,103 @@
1
+ require "forwardable"
2
+ require "shellwords"
3
+ require_relative "image_tag"
4
+
5
+ module Dmatrix
6
+ class Executor
7
+ extend Forwardable
8
+
9
+ def initialize(combination:, run_command:, log_dir:, executor: Kernel.method(:system))
10
+ @combination = combination
11
+ @run_command = run_command
12
+ @log_dir = log_dir
13
+ @executor = executor
14
+ end
15
+
16
+ def build_run
17
+ run_success = false
18
+ build_success = build
19
+
20
+ if build_success
21
+ run_success = run
22
+ end
23
+
24
+ Result.new(build_success, run_success, tag)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :combination, :run_command, :log_dir, :executor
30
+
31
+ def_delegators :image_tag, :tag
32
+
33
+ def build
34
+ executor.call("docker build %{build_args} --tag %{tag} . > #{log_path(type: "build")} 2>&1" % build_params)
35
+ end
36
+
37
+ def run
38
+ executor.call("docker run %{env_args} %{tag} %{run_command} > #{log_path(type: "run")} 2>&1" % run_params)
39
+ end
40
+
41
+ def build_params
42
+ { tag: tag, build_args: build_args }
43
+ end
44
+
45
+ def run_params
46
+ {
47
+ tag: tag,
48
+ env_args: env_args,
49
+ run_command: formatted_run_command,
50
+ }
51
+ end
52
+
53
+ def image_tag
54
+ @image_tag ||= ImageTag.new(values: combination.aspects.map(&:value))
55
+ end
56
+
57
+ def build_args
58
+ args = combination.aspects.select { |a| a[:type] == "build_arg" }
59
+
60
+ if args.empty?
61
+ return ""
62
+ end
63
+
64
+ args.map do |build_arg|
65
+ "--build-arg #{build_arg.name}=#{build_arg.value}"
66
+ end.join(" ")
67
+ end
68
+
69
+ def env_args
70
+ args = combination.aspects.select { |a| a[:type] == "env" }
71
+
72
+ if args.empty?
73
+ return ""
74
+ end
75
+
76
+ args.map do |env_arg|
77
+ "--env #{env_arg.name}=#{env_arg.value}"
78
+ end.join(" ")
79
+ end
80
+
81
+ def formatted_run_command
82
+ if run_command.nil? || run_command.empty?
83
+ return ""
84
+ end
85
+
86
+ Shellwords.join(run_command)
87
+ end
88
+
89
+ def log_path(type:)
90
+ File.join(log_dir, "#{type}-#{tag.gsub(":", "-")}.log")
91
+ end
92
+
93
+ Result = Struct.new(:build_success, :run_success, :tag) do
94
+ def success?
95
+ build_success && run_success
96
+ end
97
+
98
+ def failure?
99
+ !success?
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,26 @@
1
+ module Dmatrix
2
+ class ImageTag
3
+ def initialize(values:)
4
+ @values = values
5
+ end
6
+
7
+ def tag
8
+ "#{repo}:#{combination_tag}"
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :values
14
+
15
+ INVALID_CHARS = /[^\w\-]/
16
+ private_constant :INVALID_CHARS
17
+
18
+ def repo
19
+ File.split(Dir.pwd).last
20
+ end
21
+
22
+ def combination_tag
23
+ values.join("-").gsub(INVALID_CHARS, "-")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ module Dmatrix
2
+ class Logger
3
+ def initialize(std_out: STDOUT)
4
+ @std_out = std_out
5
+ end
6
+
7
+ def log_result(result)
8
+ message = [
9
+ result.tag,
10
+ format_status("Build", result.build_success),
11
+ format_status("Run", result.run_success)
12
+ ]
13
+
14
+ std_out.puts(message.join("\t"))
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :std_out
20
+
21
+ def format_status(label, success)
22
+ if success
23
+ "#{label}: #{green('success')}"
24
+ else
25
+ "#{label}: #{red('failure')}"
26
+ end
27
+ end
28
+
29
+ def red(text)
30
+ "\e[31m#{text}\e[0m"
31
+ end
32
+
33
+ def green(text)
34
+ "\e[32m#{text}\e[0m"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ module Dmatrix
2
+ class Matrix
3
+ def initialize(input)
4
+ @input = input
5
+ end
6
+
7
+ def combinations
8
+ dimensions = []
9
+
10
+ input.each do |type, dimension_group|
11
+ dimension_group.each do |(name, values)|
12
+ dimensions << values.map { |value| Aspect.new(type, name, value) }
13
+ end
14
+ end
15
+
16
+ dimensions.first.product(*dimensions.drop(1)).map do |aspects|
17
+ Combination.new(aspects)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :input
24
+
25
+ Combination = Struct.new(:aspects)
26
+ Aspect = Struct.new(:type, :name, :value)
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ require "fileutils"
2
+ require "parallel"
3
+ require "yaml"
4
+
5
+ require_relative "executor"
6
+ require_relative "logger"
7
+ require_relative "matrix"
8
+
9
+ module Dmatrix
10
+ class Runner
11
+ def initialize(options:, run_command:, logger: Logger.new)
12
+ @options = options
13
+ @run_command = run_command
14
+ @logger = logger
15
+ end
16
+
17
+ def call
18
+ reset_log_dir
19
+
20
+ results = Parallel.map(combinations, in_threads: 4) do |combination|
21
+ Executor.new(
22
+ combination: combination,
23
+ run_command: run_command,
24
+ log_dir: log_dir
25
+ ).build_run.tap do |result|
26
+ logger.log_result(result)
27
+ end
28
+ end
29
+
30
+ if results.any?(&:failure?)
31
+ exit(1)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :options, :run_command, :logger
38
+
39
+ def combinations
40
+ Matrix.new(input_combinations).combinations
41
+ end
42
+
43
+ def input_combinations
44
+ YAML.load_file(matrix_path).fetch("matrix")
45
+ end
46
+
47
+ def matrix_path
48
+ options.fetch(:matrix)
49
+ end
50
+
51
+ def reset_log_dir
52
+ FileUtils.mkdir_p(log_dir)
53
+ FileUtils.rm_r(log_files)
54
+ end
55
+
56
+ def log_dir
57
+ options.fetch(:log_dir)
58
+ end
59
+
60
+ def log_files
61
+ Dir.glob(File.join(log_dir, "*.log"))
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module Dmatrix
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dmatrix
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Oliver Peate
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-06-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parallel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: jet_black
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.8'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.8'
111
+ description:
112
+ email:
113
+ executables:
114
+ - dmatrix
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".matrix.yaml"
120
+ - ".rspec"
121
+ - CHANGELOG.md
122
+ - Dockerfile
123
+ - Gemfile
124
+ - Gemfile.lock
125
+ - LICENSE.txt
126
+ - README.md
127
+ - Rakefile
128
+ - bin/console
129
+ - bin/setup
130
+ - dmatrix.gemspec
131
+ - exe/dmatrix
132
+ - lib/dmatrix.rb
133
+ - lib/dmatrix/executor.rb
134
+ - lib/dmatrix/image_tag.rb
135
+ - lib/dmatrix/logger.rb
136
+ - lib/dmatrix/matrix.rb
137
+ - lib/dmatrix/runner.rb
138
+ - lib/dmatrix/version.rb
139
+ homepage: https://github.com/odlp/dmatrix
140
+ licenses:
141
+ - MIT
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubygems_version: 3.0.3
159
+ signing_key:
160
+ specification_version: 4
161
+ summary: Docker matrix runner with parallel execution
162
+ test_files: []