performa 0.1.0 → 0.2.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.
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