performa 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6435a998ab87cdad87f5dceb1ff410c8f081aabe5ef10fc360479d7bd1e97fc
4
- data.tar.gz: 2f3a401bae1c21ef9d3bb42d084b19b79dc4e61486028f2a42b9e7dc44d632e9
3
+ metadata.gz: 610de8109b1e5e60dabaf00a4082876e02aadfeb30ade8c3d1c4029002f65ad8
4
+ data.tar.gz: f67c8f636156222f878b8a8ba7ab599e4eb3d768cbf8858ec74b461cf2f6263b
5
5
  SHA512:
6
- metadata.gz: 7792a8296acd422c3d6cd0819f00b509311f57de99b042917e99232d31be9c8c17f76805dd6d501dd28b19ab128b81dd69b4ca7eb0046145bac9be8df21cb405
7
- data.tar.gz: fe0205c9b1cf8cfb9bacf9b5b2afaa0a1f077f54e1ba5faa591c1613b67adf29eb82b362782696473c5910e0174b917c3ef41a4557c139adb29a214c2b8aefdd
6
+ metadata.gz: 3e6b1b838b7b31388acfb7f63cd6f7da63ff93a1640d11fc1069f8bd3184c1ed998dee0a11ee5ba86a4a528ebb3a47dae256f4be61fcacc55b031eea1e92d4a4
7
+ data.tar.gz: 00fde074f5669f69a4b31c50db8ba6c0183abddfa25b2d53ce447fb1b2f547f9d9f110058c48b3f5b225cfd7d155f291ad6ff78baefee262bd78bbe50af7e82d
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.0-dev
1
+ 2.6.0-preview3
data/Gemfile.lock CHANGED
@@ -1,6 +1,6 @@
1
1
  GIT
2
2
  remote: https://github.com/rubocop-hq/rubocop
3
- revision: e74002ce0cf4367b3cebd580523b07ea2a59bf55
3
+ revision: 52319e485b733e6780a38986e79e7b542b0442aa
4
4
  specs:
5
5
  rubocop (0.60.0)
6
6
  jaro_winkler (~> 1.5.1)
@@ -14,7 +14,7 @@ GIT
14
14
  PATH
15
15
  remote: .
16
16
  specs:
17
- performa (0.1.0)
17
+ performa (0.2.0)
18
18
  colorize (~> 0.8)
19
19
 
20
20
  GEM
@@ -30,7 +30,7 @@ GEM
30
30
  ast (~> 2.4.0)
31
31
  powerpack (0.1.2)
32
32
  rainbow (3.0.0)
33
- rake (10.5.0)
33
+ rake (12.3.1)
34
34
  rspec (3.8.0)
35
35
  rspec-core (~> 3.8.0)
36
36
  rspec-expectations (~> 3.8.0)
@@ -59,4 +59,4 @@ DEPENDENCIES
59
59
  rubocop!
60
60
 
61
61
  BUNDLED WITH
62
- 2.0.0.pre.1
62
+ 1.17.1
data/exe/performa CHANGED
@@ -4,7 +4,7 @@
4
4
  lib = File.expand_path("../lib", __dir__)
5
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
6
 
7
- require "byebug" if ENV["DEBUG"]
7
+ require("byebug") if ENV["DEBUG"] && !Gem::Specification.find_all_by_name("byebug").empty?
8
8
  require "performa"
9
9
  require "optionparser"
10
10
 
@@ -12,7 +12,7 @@ options = Struct.new(:config_file).new
12
12
 
13
13
  OptionParser.new do |opts|
14
14
  opts.banner = [
15
- "Usage: performa [-c config-file.yml]",
15
+ "Usage: performa [-c performa-config-file.yml]",
16
16
  "Default config files considered: #{Performa::Configuration::DEFAULT_FILES.join(', ')}\n\n"
17
17
  ].join("\n")
18
18
 
@@ -25,6 +25,18 @@ OptionParser.new do |opts|
25
25
  exit 0
26
26
  end
27
27
 
28
+ opts.on("-i", "--init [.performa.yml]", "Generate a new config file") do |file|
29
+ file ||= ".performa.yml"
30
+ begin
31
+ Performa::Configuration.generate_file(file)
32
+ rescue Errno::EEXIST
33
+ puts "Error: `#{file}` already exists. Please delete it first."
34
+ exit 1
35
+ end
36
+
37
+ exit 0
38
+ end
39
+
28
40
  opts.on("-h", "--help", "Prints this help") do
29
41
  puts opts
30
42
  exit 0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Performa
4
+ class CommandResult < String
5
+ attr_accessor :success
6
+
7
+ def success?
8
+ @success
9
+ end
10
+
11
+ def failure?
12
+ !@success
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ ---
2
+
3
+ ## [Optional] Config file version (default: latest)
4
+ version: 1
5
+
6
+ ## [Required] Base docker images to run command on
7
+ images:
8
+ # - ruby:2.4
9
+ # - ruby:2.5
10
+
11
+ ## [Optional] Commands setting up each image before running command.
12
+ ## Environments generated = images * stages
13
+ # stages:
14
+ # activerecord_4:
15
+ # - gem install sqlite3
16
+ # - gem install activerecord -v=4.0.0
17
+ # activerecord_5:
18
+ # - gem install sqlite3
19
+ # - gem install activerecord -v=5.0.0
20
+
21
+ ## [Optional] Cache environments (as performa docker images)
22
+ # cache_environments: true
23
+
24
+ ## [Optional] Volumes to mount
25
+ # volumes:
26
+ # - .:/app
27
+
28
+ ## [Required] Command to run on all environments
29
+ command: |
30
+ ruby -e "puts RUBY_VERSION"
@@ -5,8 +5,7 @@ require "yaml"
5
5
  module Performa
6
6
  class Configuration
7
7
  DEFAULT_FILES = %w[
8
- performa.yml config/performa.yml
9
- spec/performa.yml test/performa.yml
8
+ .performa.yml performa.yml config/performa.yml
10
9
  ].freeze
11
10
 
12
11
  ERR_READING_CONFIG_FILE = "Could not read config file %s (%s)"
@@ -40,8 +39,7 @@ module Performa
40
39
  end
41
40
 
42
41
  def validate_data
43
- raise InvalidDataError if @data["version"].nil? ||
44
- !@data["images"]&.is_a?(Array) ||
42
+ raise InvalidDataError if !@data["images"]&.is_a?(Array) ||
45
43
  @data["command"]&.empty?
46
44
  rescue InvalidDataError
47
45
  raise Error, "Invalid config"
@@ -50,5 +48,16 @@ module Performa
50
48
  def [](name)
51
49
  @data[name]
52
50
  end
51
+
52
+ def cachable_envs?
53
+ !(@data["cache_environments"] == false || @data["stages"].nil?)
54
+ end
55
+
56
+ def self.generate_file(file)
57
+ raise Errno::EEXIST if File.exist?(file)
58
+
59
+ template_file = File.join(__dir__, "configuration-template.yml")
60
+ FileUtils.cp(template_file, file, verbose: true)
61
+ end
53
62
  end
54
63
  end
@@ -19,7 +19,7 @@ module Performa
19
19
  end
20
20
 
21
21
  def kill(container_id)
22
- run_command("docker kill #{container_id}")
22
+ run_command("docker kill #{container_id}", success_only: false)
23
23
  containers.delete(container_id)
24
24
  end
25
25
 
@@ -20,15 +20,13 @@ module Performa
20
20
  end
21
21
 
22
22
  def process_env(env, config:)
23
+ LOG.info_notice("Processing #{env.name}")
23
24
  container_id = Images.process(env, config: config)
24
- unless container_id.from_cache
25
- Stages.process(env, container_id: container_id)
26
- Images.cache_container(container_id, tag: env.hash) unless config["cache_environments"] == false
27
- end
28
-
29
- result = run_command("docker container exec #{container_id} #{config['command']}")
30
- ContainerRegistry.kill(container_id)
31
- result
25
+ run_container_command(container_id, config["command"], success_only: false)
26
+ rescue CommandFailureError => error
27
+ error.message
28
+ ensure
29
+ ContainerRegistry.kill(container_id) if container_id
32
30
  end
33
31
  end
34
32
  end
@@ -5,15 +5,24 @@ require "digest"
5
5
  module Performa
6
6
  class Environment
7
7
  def self.all(config)
8
- unless config["stages"]
9
- return config["images"].map do |image|
10
- new(image: image, volumes: config["volumes"])
11
- end
8
+ config["stages"] ? all_with_active_stages(config) : all_without_stages(config)
9
+ end
10
+
11
+ def self.all_without_stages(config)
12
+ config["images"].map do |image|
13
+ new(image: image, volumes: config["volumes"])
12
14
  end
15
+ end
16
+
17
+ def self.all_with_active_stages(config)
18
+ skipped = config["skip"]&.flat_map { |image, stages_names| [image].product(stages_names) }
13
19
 
14
- config["images"].product(config["stages"].to_a).map do |image, stage|
20
+ config["images"].product(config["stages"].to_a).map do |image, config_stage|
21
+ next if skipped&.include?([image, config_stage[0]])
22
+
23
+ stage = Stage.from_config(config_stage, image: image)
15
24
  new(image: image, stage: stage, volumes: config["volumes"])
16
- end
25
+ end.compact
17
26
  end
18
27
 
19
28
  attr_reader :image, :stage, :volumes, :name, :hash
@@ -23,15 +32,10 @@ module Performa
23
32
  @stage = stage
24
33
  @volumes = volumes || []
25
34
  assign_name
26
- assign_hash
27
35
  end
28
36
 
29
37
  def assign_name
30
- @name = @image.tr(":", "_") + ("-#{@stage[0]}" if @stage).to_s
31
- end
32
-
33
- def assign_hash
34
- @hash = Digest::SHA1.hexdigest(@image + @stage.to_s)
38
+ @name = @image.tr(":", "_") + ("-#{@stage.name}" if @stage).to_s
35
39
  end
36
40
  end
37
41
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Performa
4
+ Error = Class.new(StandardError)
5
+ CommandFailureError = Class.new(Error)
6
+ end
@@ -8,22 +8,41 @@ module Performa
8
8
  CACHED_IMAGES_NAME = "performa_env"
9
9
 
10
10
  def process(env, config:)
11
- unless config["cache_environments"] == false
11
+ if config.cachable_envs?
12
12
  container_id = container_id_for_cached_image(env)
13
13
  return container_id if container_id
14
14
  end
15
15
 
16
16
  pull_if_missing(env.image)
17
- id = start_image_container(env.image, volumes: env.volumes)
18
- ContainerId.new(id)
17
+ container_id = start_image_container(env.image, volumes: env.volumes)
18
+
19
+ if env.stage&.commands
20
+ run_and_cache_commands(
21
+ env.stage.commands,
22
+ cacheable_envs: config.cachable_envs?,
23
+ container_id: container_id
24
+ )
25
+ end
26
+
27
+ container_id
19
28
  end
20
29
 
21
30
  def container_id_for_cached_image(env)
22
- cached_image = "#{CACHED_IMAGES_NAME}:#{env.hash}"
23
- return unless exists?(cached_image)
31
+ commands = env.stage.commands
32
+ last_cached_stage_command = commands.reverse.find(&:cache)
33
+ return unless last_cached_stage_command
34
+
35
+ container_id = start_image_container(last_cached_stage_command.cache, volumes: env.volumes)
36
+ uncached_commands = commands[commands.index(last_cached_stage_command) + 1..-1]
37
+ run_and_cache_commands(uncached_commands, container_id: container_id)
38
+ container_id
39
+ end
24
40
 
25
- id = start_image_container(cached_image, volumes: env.volumes)
26
- ContainerId.from_cache(id)
41
+ def run_and_cache_commands(commands, cacheable_envs: true, container_id:)
42
+ commands.each do |command|
43
+ run_container_command(container_id, command.value, success_only: false)
44
+ cache_container(container_id, tag: command.hash) if cacheable_envs
45
+ end
27
46
  end
28
47
 
29
48
  def pull(image)
@@ -31,11 +50,15 @@ module Performa
31
50
  end
32
51
 
33
52
  def pull_if_missing(image)
34
- pull(image) unless exists?(image)
53
+ pull(image) unless presence(image)
54
+ end
55
+
56
+ def presence(image)
57
+ image unless run_command("docker images -q #{image}").empty?
35
58
  end
36
59
 
37
- def exists?(image)
38
- !run_command("docker images -q #{image}").empty?
60
+ def cache_presence(hash)
61
+ presence("#{CACHED_IMAGES_NAME}:#{hash}")
39
62
  end
40
63
 
41
64
  def start_image_container(image, volumes: [])
@@ -18,7 +18,21 @@ module Performa
18
18
  end
19
19
  end
20
20
 
21
- def LOG.success(message)
22
- LOG.info(message.colorize(:green))
21
+ class << LOG
22
+ def info_success(message)
23
+ LOG.info(message.colorize(:green))
24
+ end
25
+
26
+ def info_error(message)
27
+ LOG.info(message.colorize(:red))
28
+ end
29
+
30
+ def info_warning(message)
31
+ LOG.info(message.colorize(:yellow))
32
+ end
33
+
34
+ def info_notice(message)
35
+ LOG.info(message.colorize(:light_blue))
36
+ end
23
37
  end
24
38
  end
@@ -1,43 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pty"
4
3
  require "English"
4
+ require "open3"
5
+ require "shellwords"
5
6
 
6
7
  module Performa
7
8
  module ShellHelper
8
9
  def run_command(command, success_only: true, no_capture: false)
9
- LOG.success("Running `#{command}` ...")
10
+ LOG.info("Running `#{command.colorize(:light_yellow)}` ...")
10
11
 
11
- if no_capture
12
- system(command)
13
- result = ""
14
- else
15
- result = pty_spawn(command)
16
- end
17
-
18
- raise "(non-zero exit code)" if success_only && !$CHILD_STATUS.success?
12
+ exit_status, result_str = no_capture ? run_no_capture_command(command) : run_capture_command(command)
13
+ raise "(non-zero exit code: #{exit_status.exitstatus})" if success_only && !exit_status.success?
19
14
 
20
- result
15
+ CommandResult.new(result_str).tap do |result|
16
+ result.success = exit_status.success?
17
+ end
21
18
  rescue StandardError => e
22
19
  raise Error, <<~MSG
23
20
  Error running the command `#{command}`:
24
21
  => error: #{e.message}
25
- => command output: #{result}
22
+ => command output: #{result_str}
26
23
  MSG
27
24
  end
28
25
 
29
- def pty_spawn(command)
30
- result = +""
31
- PTY.spawn(command) do |stdout, _stdin, _pid|
32
- stdout.each do |line|
33
- LOG.info(line.strip)
34
- result << line
26
+ def run_no_capture_command(command)
27
+ system(command)
28
+ [$CHILD_STATUS, ""]
29
+ end
30
+
31
+ def run_capture_command(command)
32
+ exit_status = nil
33
+ result_str = +""
34
+
35
+ Open3.popen2e(command) do |_stdin, stdout_and_stderr, wait_thr|
36
+ stdout_and_stderr.each do |line|
37
+ result_str << line
35
38
  end
39
+ exit_status = wait_thr.value
36
40
  end
41
+ exit_status.success? ? LOG.info_success(result_str) : LOG.info_error(result_str)
37
42
 
38
- Process.wait unless $CHILD_STATUS.exited?
43
+ [exit_status, result_str]
44
+ end
39
45
 
40
- result
46
+ def run_container_command(container_id, command, **options)
47
+ run_command("docker container exec #{container_id} sh -c #{Shellwords.escape(command)}", options)
41
48
  end
42
49
  end
43
50
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module Performa
6
+ class Stage
7
+ def self.from_config(config_stage, image:)
8
+ hash = Digest::SHA1.hexdigest(image)
9
+
10
+ commands = config_stage[1].map do |config_command|
11
+ hash = Digest::SHA1.hexdigest(hash + config_command)
12
+ StageCommand.new(config_command, hash: hash)
13
+ end
14
+
15
+ new(name: config_stage[0], commands: commands)
16
+ end
17
+
18
+ attr_reader :name, :commands, :hash
19
+
20
+ def initialize(name:, commands:)
21
+ @name = name
22
+ @commands = commands
23
+ @hash = Digest::SHA1.hexdigest(@name + @commands.map(&:hash).join)
24
+ end
25
+ end
26
+
27
+ class StageCommand
28
+ attr_reader :value, :hash
29
+
30
+ def initialize(value, hash:)
31
+ @value = value
32
+ @hash = hash
33
+ end
34
+
35
+ def cache
36
+ @cache ||= Images.cache_presence(@hash)
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Performa
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/performa.rb CHANGED
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "performa/version"
4
+ require "performa/errors"
4
5
  require "performa/logger"
5
6
  require "performa/configuration"
7
+ require "performa/command_result"
6
8
  require "performa/shell_helper"
7
9
  require "performa/container_registry"
8
10
  require "performa/environment"
9
- require "performa/container_id"
11
+ require "performa/stage"
10
12
  require "performa/images"
11
- require "performa/stages"
12
13
  require "performa/coordinator"
13
14
  require "performa/results_helper"
14
15
 
15
16
  module Performa
16
- Error = Class.new(StandardError)
17
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: performa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christophe Maximin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-19 00:00:00.000000000 Z
11
+ date: 2018-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -46,16 +46,18 @@ files:
46
46
  - bin/setup
47
47
  - exe/performa
48
48
  - lib/performa.rb
49
+ - lib/performa/command_result.rb
50
+ - lib/performa/configuration-template.yml
49
51
  - lib/performa/configuration.rb
50
- - lib/performa/container_id.rb
51
52
  - lib/performa/container_registry.rb
52
53
  - lib/performa/coordinator.rb
53
54
  - lib/performa/environment.rb
55
+ - lib/performa/errors.rb
54
56
  - lib/performa/images.rb
55
57
  - lib/performa/logger.rb
56
58
  - lib/performa/results_helper.rb
57
59
  - lib/performa/shell_helper.rb
58
- - lib/performa/stages.rb
60
+ - lib/performa/stage.rb
59
61
  - lib/performa/version.rb
60
62
  - performa.gemspec
61
63
  homepage: https://github.com/christophemaximin/performa
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Performa
4
- class ContainerId < String
5
- attr_accessor :from_cache
6
-
7
- def initialize(*args)
8
- @from_cache = false
9
- super
10
- end
11
-
12
- def self.from_cache(string)
13
- str = new(string)
14
- str.from_cache = true
15
- str
16
- end
17
- end
18
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Performa
4
- module Stages
5
- module_function
6
-
7
- extend ShellHelper
8
-
9
- def process(env, container_id:)
10
- return unless env.stage
11
-
12
- env.stage[1].each do |command|
13
- run_command("docker container exec #{container_id} #{command}")
14
- end
15
- end
16
- end
17
- end