producer-core 0.1.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.
Files changed (65) hide show
  1. data/.gitignore +3 -0
  2. data/Gemfile +5 -0
  3. data/Guardfile +11 -0
  4. data/LICENSE +30 -0
  5. data/Rakefile +13 -0
  6. data/bin/producer +5 -0
  7. data/features/actions/echo.feature +12 -0
  8. data/features/actions/sh.feature +39 -0
  9. data/features/cli/usage.feature +9 -0
  10. data/features/recipes/env.feature +9 -0
  11. data/features/recipes/evaluation.feature +10 -0
  12. data/features/recipes/source.feature +16 -0
  13. data/features/recipes/target.feature +12 -0
  14. data/features/steps/recipe_steps.rb +7 -0
  15. data/features/support/env.rb +0 -0
  16. data/features/support/env_aruba.rb +16 -0
  17. data/features/support/env_cucumber-doc_string.rb +23 -0
  18. data/features/support/ssh.rb +106 -0
  19. data/features/tasks/condition.feature +14 -0
  20. data/features/tasks/evaluation.feature +12 -0
  21. data/features/tests/has_env.feature +32 -0
  22. data/lib/producer/core/action.rb +12 -0
  23. data/lib/producer/core/actions/echo.rb +11 -0
  24. data/lib/producer/core/actions/shell_command.rb +11 -0
  25. data/lib/producer/core/cli.rb +43 -0
  26. data/lib/producer/core/condition/dsl.rb +35 -0
  27. data/lib/producer/core/condition.rb +26 -0
  28. data/lib/producer/core/env.rb +21 -0
  29. data/lib/producer/core/errors.rb +7 -0
  30. data/lib/producer/core/interpreter.rb +13 -0
  31. data/lib/producer/core/recipe/dsl.rb +48 -0
  32. data/lib/producer/core/recipe.rb +15 -0
  33. data/lib/producer/core/remote/environment.rb +27 -0
  34. data/lib/producer/core/remote.rb +40 -0
  35. data/lib/producer/core/task/dsl.rb +42 -0
  36. data/lib/producer/core/task.rb +21 -0
  37. data/lib/producer/core/test.rb +12 -0
  38. data/lib/producer/core/tests/has_env.rb +11 -0
  39. data/lib/producer/core/version.rb +5 -0
  40. data/lib/producer/core.rb +22 -0
  41. data/producer-core.gemspec +28 -0
  42. data/spec/fixtures/recipes/empty.rb +1 -0
  43. data/spec/fixtures/recipes/throw.rb +1 -0
  44. data/spec/producer/core/action_spec.rb +21 -0
  45. data/spec/producer/core/actions/echo_spec.rb +21 -0
  46. data/spec/producer/core/actions/shell_command_spec.rb +27 -0
  47. data/spec/producer/core/cli_spec.rb +102 -0
  48. data/spec/producer/core/condition/dsl_spec.rb +102 -0
  49. data/spec/producer/core/condition_spec.rb +96 -0
  50. data/spec/producer/core/env_spec.rb +54 -0
  51. data/spec/producer/core/interpreter_spec.rb +41 -0
  52. data/spec/producer/core/recipe/dsl_spec.rb +131 -0
  53. data/spec/producer/core/recipe_spec.rb +43 -0
  54. data/spec/producer/core/remote/environment_spec.rb +34 -0
  55. data/spec/producer/core/remote_spec.rb +87 -0
  56. data/spec/producer/core/task/dsl_spec.rb +131 -0
  57. data/spec/producer/core/task_spec.rb +91 -0
  58. data/spec/producer/core/test_spec.rb +30 -0
  59. data/spec/producer/core/tests/has_env_spec.rb +41 -0
  60. data/spec/spec_helper.rb +16 -0
  61. data/spec/support/exit_helpers.rb +6 -0
  62. data/spec/support/fixtures_helpers.rb +7 -0
  63. data/spec/support/net_ssh_story_helpers.rb +17 -0
  64. data/spec/support/tests_helpers.rb +9 -0
  65. metadata +230 -0
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ /Gemfile-custom.rb
2
+ /Gemfile.lock
3
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ eval File.read('Gemfile-custom.rb') if File.exist?('Gemfile-custom.rb')
data/Guardfile ADDED
@@ -0,0 +1,11 @@
1
+ guard :cucumber, cli: '--format pretty --quiet' do
2
+ watch(%r{\Afeatures/.+\.feature\z})
3
+ watch(%r{\Afeatures/support/.+\.rb\z}) { 'features' }
4
+ watch(%r{\Afeatures/step_definitions/.+_steps\.rb\z}) { 'features' }
5
+ end
6
+
7
+ guard :rspec do
8
+ watch(%r{\Aspec/.+_spec\.rb\z})
9
+ watch(%r{\Alib/(.+)\.rb\z}) { |m| "spec/#{m[1]}_spec.rb" }
10
+ watch('spec/spec_helper.rb') { 'spec' }
11
+ end
data/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright 2013 Thibault Jouan. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in
12
+ the documentation and/or other materials provided with the
13
+ distribution.
14
+
15
+ * Neither the name of the software nor the names of its contributors
16
+ may be used to endorse or promote products derived from this
17
+ software without specific prior written permission.
18
+
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS
24
+ BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
30
+ IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'cucumber/rake/task'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc 'Run all scenarios'
5
+ Cucumber::Rake::Task.new(:features) do |t|
6
+ t.cucumber_opts = '--tags ~@sshd'
7
+ end
8
+
9
+ desc 'Run all specs'
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+
13
+ task default: [:features, :spec]
data/bin/producer ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'producer/core'
4
+
5
+ Producer::Core::CLI.new(ARGV).run!
@@ -0,0 +1,12 @@
1
+ Feature: `echo' task action
2
+
3
+ Scenario: ouputs text
4
+ Given a recipe with:
5
+ """
6
+ task :some_task do
7
+ echo 'hello'
8
+ end
9
+ """
10
+ When I execute the recipe
11
+ Then the exit status must be 0
12
+ And the output must contain exactly "hello\n"
@@ -0,0 +1,39 @@
1
+ @sshd
2
+ Feature: sh task action
3
+
4
+ Scenario: executes command
5
+ Given a recipe with:
6
+ """
7
+ target 'some_host.test'
8
+
9
+ task :some_task do
10
+ sh '\true'
11
+ end
12
+ """
13
+ When I execute the recipe
14
+ Then the exit status must be 0
15
+
16
+ Scenario: forwards standard ouput
17
+ Given a recipe with:
18
+ """
19
+ target 'some_host.test'
20
+
21
+ task :some_task do
22
+ sh '\echo from remote'
23
+ end
24
+ """
25
+ When I execute the recipe
26
+ Then the output must contain "from remote"
27
+
28
+ Scenario: aborts on failed command execution
29
+ Given a recipe with:
30
+ """
31
+ target 'some_host.test'
32
+
33
+ task :some_task do
34
+ sh '\false'
35
+ sh '\echo after_fail'
36
+ end
37
+ """
38
+ When I execute the recipe
39
+ Then the output must not contain "after_fail"
@@ -0,0 +1,9 @@
1
+ Feature: CLI usage
2
+
3
+ Scenario: prints the usage when an argument is missing
4
+ When I run `producer`
5
+ Then the exit status must be 64
6
+ And the output must contain exactly:
7
+ """
8
+ Usage: producer recipe_file
9
+ """
@@ -0,0 +1,9 @@
1
+ Feature: `env' recipe keyword
2
+
3
+ Scenario: exposes the internal env object
4
+ Given a recipe with:
5
+ """
6
+ env
7
+ """
8
+ When I execute the recipe
9
+ Then the exit status must be 0
@@ -0,0 +1,10 @@
1
+ Feature: recipe evaluation
2
+
3
+ Scenario: evaluates ruby code in a recipe
4
+ Given a recipe with:
5
+ """
6
+ puts 'hello from recipe'
7
+ """
8
+ When I execute the recipe
9
+ Then the exit status must be 0
10
+ And the output must contain exactly "hello from recipe\n"
@@ -0,0 +1,16 @@
1
+ Feature: `source' recipe keyword
2
+
3
+ Background:
4
+ Given a recipe with:
5
+ """
6
+ source 'sourced_recipe'
7
+ """
8
+
9
+ Scenario: requires a recipe file
10
+ Given a file named "sourced_recipe.rb" with:
11
+ """
12
+ puts 'sourced recipe'
13
+ """
14
+ When I execute the recipe
15
+ Then the exit status must be 0
16
+ And the output must contain "sourced recipe"
@@ -0,0 +1,12 @@
1
+ Feature: `target' recipe keyword
2
+
3
+ Scenario: registers the target host on which tasks should be applied
4
+ Given a recipe with:
5
+ """
6
+ target 'some_host.example'
7
+
8
+ puts env.target
9
+ """
10
+ When I execute the recipe
11
+ Then the exit status must be 0
12
+ And the output must contain exactly "some_host.example\n"
@@ -0,0 +1,7 @@
1
+ Given(/^a recipe with:$/) do |recipe_body|
2
+ write_file 'recipe.rb', recipe_body
3
+ end
4
+
5
+ When(/^I execute the recipe$/) do
6
+ run_simple('producer recipe.rb', false)
7
+ end
File without changes
@@ -0,0 +1,16 @@
1
+ module Cucumber
2
+ class Runtime
3
+ alias :old_step_match :step_match
4
+
5
+ def step_match(step_name, name_to_report = nil)
6
+ if step_name.include? ' must '
7
+ name_to_report = step_name.dup
8
+ step_name.gsub! ' must ', ' should '
9
+ end
10
+
11
+ old_step_match(step_name, name_to_report)
12
+ end
13
+ end
14
+ end
15
+
16
+ require 'aruba/cucumber'
@@ -0,0 +1,23 @@
1
+ require 'cucumber/formatter/pretty'
2
+
3
+ module Cucumber
4
+ module Ast
5
+ class DocString
6
+ alias :old_initialize :initialize
7
+
8
+ def initialize(string, content_type)
9
+ old_initialize(string + "\n", content_type)
10
+ end
11
+ end
12
+ end
13
+
14
+ module Formatter
15
+ class Pretty
16
+ alias :old_doc_string :doc_string
17
+
18
+ def doc_string(string)
19
+ old_doc_string(string.chomp)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,106 @@
1
+ # FIXME: should be extracted as a separate gem `cucumber-sshd?'.
2
+ class SSHServer
3
+ include Aruba::Api
4
+
5
+ DEFAULT_LISTEN_PORT = 2222
6
+ KEY_PATH = 'etc/ssh_host_rsa_key'
7
+ KEY_PUB_PATH = KEY_PATH.dup << '.pub'
8
+ SSHD_CONFIG_PATH = 'etc/sshd_config'
9
+ SSH_CONFIG_PATH = '.ssh/config'
10
+
11
+ attr_accessor :listen_port
12
+
13
+ def initialize(base_path)
14
+ @base_path = base_path
15
+ @listen_port = ENV['PRODUCER_TEST_SSHD_PORT'] ?
16
+ ENV['PRODUCER_TEST_SSHD_PORT'] :
17
+ DEFAULT_LISTEN_PORT
18
+ end
19
+
20
+ def start
21
+ in_current_dir do
22
+ @pid = fork do
23
+ $stderr.reopen '/dev/null'
24
+ exec "/usr/sbin/sshd -f etc/sshd_config -Deq"
25
+ end
26
+ end
27
+ end
28
+
29
+ def stop
30
+ Process.kill('TERM', @pid)
31
+ Process.wait(@pid)
32
+ end
33
+
34
+ def make_env
35
+ %w[etc .ssh].map { |e| create_dir e }
36
+
37
+ write_file KEY_PATH, <<-eoh
38
+ -----BEGIN RSA PRIVATE KEY-----
39
+ MIIEpAIBAAKCAQEA7EVDKeM7NYCGGVJw0wWLGCoptMFSR7DobhbEx2bAQbWDLFBF
40
+ 7S9bXpW/ddebFA4GBkHVriNPwu/IGjIgO3tivVcy6iguNKdYRABlSfpeAs+OdCzK
41
+ hyEWhmKnFMZs2MhPiJg/KUep2gFZLoEcs9vkk37+cLcuUVkXVOSPXlYCuHjgkwaN
42
+ 9ij/KvCcLP4tw83H3Pyh1Wn/Y4k1+3nWN2S3RgRHF9RjEx7nm2FmXcZq1wygPe5G
43
+ 2bQNvZKykL+wlKIHwKiM4AWpVClczOiljNv9KROhGWanIZNNBIDTnikxFvY0FyoP
44
+ 1qvRrE8BlqxNeyG/vYNmXsv8xpO0MOF65z3d2QIDAQABAoIBAHL8dmZxX0R3i0vR
45
+ knUwsnQNOQTuPPQFBelmDViaCiEwjGlJm+6F6KrMqERarO+Cr63l5m98YfoWJkWR
46
+ dZxdPT22rWHGMk6PzuYxZxoszgoCJ2skzWAcW1EFvBjhROHwAr0Qk1Ssut4NX/DB
47
+ B04FS2X5HS2QCOuwNymqnpejtmk+A2hv9bGVzj0X614gX3h5+0dImGrYE0Lu+Abf
48
+ 5fvWhN5nxgK5CVlU7WM09WxyHj9lBXI+W2dgTl6w3QJfBBQTkarLmDpwIeErq9xc
49
+ al2qHj60nYC+RdFopuLfJWKiObdKRFpuPFYKbTA9nJz9T61zAF+LaDZ1mvdTuQmz
50
+ jJEJ0nECgYEA+o2uJcDOCD2LBp2LqeQcGk1PrFLpSQ5921B10SxDWhygs2+UlBCW
51
+ 7t/qurqjEUZ91l3TAlUM7ViB/EkXk/xWJRJCzlmWRXfht9LPQTWlnDox/w8uSV2g
52
+ 3VwPx1xpju2PHO7Vsk6dsQsyoro14qNhYa9m1lBHA1TtJ/RLWauvnmUCgYEA8WgZ
53
+ MthPXi/clDg2DlROGOEWd7WgaErEz578HWnegcMrHb8RV/XW21CO2UegHykzUlxz
54
+ vJxAqhQeKJbP7T8uzuCZnkZqBqPh5PJT1XqxZibTeQvqYLzbIiKqmDrZWuRJvbLL
55
+ kPxwYEG8R8nl9Dk1tLHuTQWWa5Q49he1cDss4GUCgYEA7WMBRZnIW3xb1Xe9VMjg
56
+ a3cmbqHbj7FgQ0OXbQigA6euBnRIdITHTCnxDtw4Fe0Q2uLoQoRsjA/YkDx8T2S8
57
+ BcGodDPjMYxk2rKsVR9L+poUtpEejLpd6H0KIhwHkzi26HXNGHRt6ckvP4hn94RO
58
+ hqwWJiXHMnvrenh2T85fxRUCgYEA7m06NhWejhAHc/zwpsZtO/VUE3e3rknqiIUl
59
+ zIc71D3G3+JOZunQA1xVOhSb+SrgHYBibu6Ej3a/MqeBRXkZ6gm6r7AsF9LU0SLl
60
+ 2fsMKzA9vVgfbNwaMmS6yQ+WjUbb7hghJlmtQ+So6N5n2AaJHKaADmJuZmJGwAg6
61
+ k1ZexGECgYAFc+GjjFOPxC+Qg6z8261PnDffBK0o+0/EIq3cGA7Cp5pEo/hhMjDl
62
+ W7CLjAGkok9W+rr9XwwXWCBuJmPh2jYeNaQljrHt9hIAOAxoYdbweb9oSo5SkcHv
63
+ iDjcFK8S1e5vnlZAh9xH1WMCEsaz1WNqWm7CZOayN2LFn6Ed9seYYg==
64
+ -----END RSA PRIVATE KEY-----
65
+ eoh
66
+
67
+ write_file KEY_PUB_PATH, <<-eoh
68
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsRUMp4zs1gIYZUnDTBYsYKim0wVJHsOhuFsTHZsBBtYMsUEXtL1telb9115sUDgYGQdWuI0/C78gaMiA7e2K9VzLqKC40p1hEAGVJ+l4Cz450LMqHIRaGYqcUxmzYyE+ImD8pR6naAVkugRyz2+STfv5wty5RWRdU5I9eVgK4eOCTBo32KP8q8Jws/i3Dzcfc/KHVaf9jiTX7edY3ZLdGBEcX1GMTHuebYWZdxmrXDKA97kbZtA29krKQv7CUogfAqIzgBalUKVzM6KWM2/0pE6EZZqchk00EgNOeKTEW9jQXKg/Wq9GsTwGWrE17Ib+9g2Zey/zGk7Qw4XrnPd3Z
69
+ eoh
70
+
71
+ write_file SSHD_CONFIG_PATH, <<-eoh
72
+ Port #{listen_port}
73
+ ListenAddress ::1
74
+
75
+ Protocol 2
76
+ HostKey #{File.expand_path @base_path}/#{KEY_PATH}
77
+ PidFile /dev/null
78
+ UsePrivilegeSeparation no
79
+ ForceCommand HOME=#{File.expand_path @base_path} sh -c "cd ~; $SSH_ORIGINAL_COMMAND"
80
+ eoh
81
+
82
+ write_file SSH_CONFIG_PATH, <<-eoh
83
+ Host some_host.test
84
+ HostName localhost
85
+ Port #{listen_port}
86
+ eoh
87
+ end
88
+ end
89
+
90
+
91
+ Before('@sshd') do
92
+ @_sshd = SSHServer.new(current_dir)
93
+ @_sshd.make_env
94
+ @_sshd.start
95
+
96
+ ENV['HOME'] = File.expand_path current_dir
97
+
98
+ # FIXME: we might need to wait until sshd accepts connections, polling isn't
99
+ # really acceptable, another workaround might be to execute sshd with
100
+ # LD_PRELOAD and a hook, so we could block here until the hook release the
101
+ # blocking call.
102
+ end
103
+
104
+ After('@sshd') do
105
+ @_sshd.stop
106
+ end
@@ -0,0 +1,14 @@
1
+ Feature: `condition' task keyword
2
+
3
+ Scenario: prevents task actions application when condition is not met
4
+ Given a recipe with:
5
+ """
6
+ task :hello do
7
+ condition { false }
8
+
9
+ echo 'evaluated'
10
+ end
11
+ """
12
+ When I execute the recipe
13
+ Then the exit status must be 0
14
+ And the output must not contain "evaluated"
@@ -0,0 +1,12 @@
1
+ Feature: task evaluation
2
+
3
+ Scenario: evaluates ruby code in task blocks
4
+ Given a recipe with:
5
+ """
6
+ task :hello do
7
+ puts 'hello from recipe'
8
+ end
9
+ """
10
+ When I execute the recipe
11
+ Then the exit status must be 0
12
+ And the output must contain exactly "hello from recipe\n"
@@ -0,0 +1,32 @@
1
+ @sshd
2
+ Feature: `has_env' condition keyword
3
+
4
+ Scenario: succeeds when remote environment variable is defined
5
+ Given a recipe with:
6
+ """
7
+ target 'some_host.test'
8
+
9
+ task :testing_env_var_definition do
10
+ condition { has_env :shell }
11
+
12
+ echo 'evaluated'
13
+ end
14
+ """
15
+ When I execute the recipe
16
+ Then the exit status must be 0
17
+ And the output must contain "evaluated"
18
+
19
+ Scenario: fails when remote environment variable is not defined
20
+ Given a recipe with:
21
+ """
22
+ target 'some_host.test'
23
+
24
+ task :testing_env_var_definition do
25
+ condition { has_env :inexistent_var }
26
+
27
+ echo 'evaluated'
28
+ end
29
+ """
30
+ When I execute the recipe
31
+ Then the exit status must be 0
32
+ And the output must not contain "evaluated"
@@ -0,0 +1,12 @@
1
+ module Producer
2
+ module Core
3
+ class Action
4
+ attr_accessor :env, :arguments
5
+
6
+ def initialize(env, *args)
7
+ @env = env
8
+ @arguments = args
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module Producer
2
+ module Core
3
+ module Actions
4
+ class Echo < Action
5
+ def apply
6
+ env.output arguments.first
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Producer
2
+ module Core
3
+ module Actions
4
+ class ShellCommand < Action
5
+ def apply
6
+ env.output env.remote.execute(arguments.first)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ module Producer
2
+ module Core
3
+ class CLI
4
+ attr_reader :arguments
5
+
6
+ USAGE = "Usage: #{File.basename $0} recipe_file"
7
+
8
+ def initialize(arguments, stdout = $stdout)
9
+ @stdout = stdout
10
+ @arguments = arguments
11
+ end
12
+
13
+ def run!
14
+ check_arguments!
15
+ interpreter.process recipe.tasks
16
+ end
17
+
18
+ def check_arguments!
19
+ print_usage_and_exit(64) unless @arguments.length == 1
20
+ end
21
+
22
+ def env
23
+ @env ||= Env.new
24
+ end
25
+
26
+ def recipe
27
+ @recipe ||= Recipe.evaluate_from_file(@arguments.first, env)
28
+ end
29
+
30
+ def interpreter
31
+ @interpreter ||= Interpreter.new
32
+ end
33
+
34
+ private
35
+
36
+ def print_usage_and_exit(status)
37
+ @stdout.puts USAGE
38
+
39
+ exit status
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,35 @@
1
+ module Producer
2
+ module Core
3
+ class Condition
4
+ class DSL
5
+ class << self
6
+ def evaluate(env, &block)
7
+ dsl = new(env, &block)
8
+ return_value = dsl.evaluate
9
+ Condition.new(dsl.tests, return_value)
10
+ end
11
+
12
+ def define_test(keyword, klass)
13
+ define_method(keyword) do |*args|
14
+ @tests << klass.new(@env, *args)
15
+ end
16
+ end
17
+ end
18
+
19
+ define_test :has_env, Tests::HasEnv
20
+
21
+ attr_accessor :tests
22
+
23
+ def initialize(env, &block)
24
+ @env = env
25
+ @block = block
26
+ @tests = []
27
+ end
28
+
29
+ def evaluate
30
+ instance_eval &@block
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ module Producer
2
+ module Core
3
+ class Condition
4
+ class << self
5
+ def evaluate(env, &block)
6
+ DSL.evaluate(env, &block)
7
+ end
8
+ end
9
+
10
+ def initialize(tests, return_value = nil)
11
+ @tests = tests
12
+ @return_value = return_value
13
+ end
14
+
15
+ def met?
16
+ return !!@return_value if @tests.empty?
17
+ @tests.each { |t| return false unless t.success? }
18
+ true
19
+ end
20
+
21
+ def !
22
+ !met?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ module Producer
2
+ module Core
3
+ class Env
4
+ attr_writer :output
5
+ attr_accessor :target
6
+
7
+ def initialize
8
+ @output = $stdout
9
+ @target = nil
10
+ end
11
+
12
+ def output(str)
13
+ @output.puts str
14
+ end
15
+
16
+ def remote
17
+ @remote ||= Remote.new(target)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module Producer
2
+ module Core
3
+ Error = Class.new(StandardError)
4
+ ConditionNotMetError = Class.new(Error)
5
+ RemoteCommandExecutionError = Class.new(Error)
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Producer
2
+ module Core
3
+ class Interpreter
4
+ def process(tasks)
5
+ tasks.each { |t| process_task t }
6
+ end
7
+
8
+ def process_task(task)
9
+ task.actions.each(&:apply) if task.condition_met?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ module Producer
2
+ module Core
3
+ class Recipe
4
+ class DSL
5
+ attr_reader :tasks
6
+
7
+ def self.evaluate(code, env)
8
+ dsl = new(code).evaluate(env)
9
+ Recipe.new(dsl.tasks)
10
+ end
11
+
12
+ def initialize(code = nil, &block)
13
+ @code = code
14
+ @block = block
15
+ @tasks = []
16
+ end
17
+
18
+ def evaluate(env)
19
+ @env = env
20
+ if @code
21
+ instance_eval @code
22
+ else
23
+ instance_eval &@block
24
+ end
25
+ self
26
+ end
27
+
28
+ private
29
+
30
+ def env
31
+ @env
32
+ end
33
+
34
+ def source(filepath)
35
+ instance_eval File.read("./#{filepath}.rb"), "#{filepath}.rb"
36
+ end
37
+
38
+ def target(hostname)
39
+ env.target = hostname
40
+ end
41
+
42
+ def task(name, &block)
43
+ @tasks << Task.evaluate(name, env, &block)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ module Producer
2
+ module Core
3
+ class Recipe
4
+ attr_accessor :tasks
5
+
6
+ def self.evaluate_from_file(filepath, env)
7
+ DSL.evaluate(File.read(filepath), env)
8
+ end
9
+
10
+ def initialize(tasks = [])
11
+ @tasks = tasks
12
+ end
13
+ end
14
+ end
15
+ end