taketo 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/Gemfile +1 -1
  2. data/Gemfile.lock +3 -3
  3. data/README.md +20 -7
  4. data/VERSION +1 -1
  5. data/bin/taketo +30 -31
  6. data/features/config.feature +33 -3
  7. data/features/config_validation.feature +29 -4
  8. data/features/connect_to_server.feature +2 -1
  9. data/features/error_handling.feature +1 -0
  10. data/features/help.feature +5 -3
  11. data/features/support/env.rb +2 -0
  12. data/lib/taketo/associated_nodes.rb +90 -0
  13. data/lib/taketo/commands/ssh_command.rb +0 -4
  14. data/lib/taketo/config_printer_visitor.rb +68 -0
  15. data/lib/taketo/config_traverser.rb +55 -0
  16. data/lib/taketo/config_validator.rb +33 -34
  17. data/lib/taketo/config_visitor.rb +29 -0
  18. data/lib/taketo/constructs/base_construct.rb +9 -54
  19. data/lib/taketo/constructs/command.rb +9 -1
  20. data/lib/taketo/constructs/environment.rb +11 -1
  21. data/lib/taketo/constructs/project.rb +5 -0
  22. data/lib/taketo/constructs/server.rb +7 -4
  23. data/lib/taketo/dsl.rb +13 -2
  24. data/lib/taketo/support/named_nodes_collection.rb +10 -0
  25. data/lib/taketo/support.rb +0 -1
  26. data/lib/taketo.rb +2 -1
  27. data/spec/integration/dsl_integration_spec.rb +1 -1
  28. data/spec/lib/taketo/associated_nodes_spec.rb +101 -0
  29. data/spec/lib/taketo/commands/ssh_command_spec.rb +5 -15
  30. data/spec/lib/taketo/config_printer_visitor_spec.rb +112 -0
  31. data/spec/lib/taketo/config_traverser_spec.rb +63 -0
  32. data/spec/lib/taketo/config_validator_spec.rb +53 -39
  33. data/spec/lib/taketo/config_visitor_spec.rb +51 -0
  34. data/spec/lib/taketo/constructs/base_construct_spec.rb +7 -70
  35. data/spec/lib/taketo/constructs/command_spec.rb +20 -8
  36. data/spec/lib/taketo/constructs/config_spec.rb +2 -6
  37. data/spec/lib/taketo/constructs/environment_spec.rb +12 -7
  38. data/spec/lib/taketo/constructs/project_spec.rb +10 -4
  39. data/spec/lib/taketo/constructs/server_spec.rb +25 -16
  40. data/spec/lib/taketo/constructs_factory_spec.rb +12 -12
  41. data/spec/lib/taketo/destination_resolver_spec.rb +32 -32
  42. data/spec/lib/taketo/dsl_spec.rb +115 -98
  43. data/spec/lib/taketo/support/named_nodes_collection_spec.rb +49 -21
  44. data/spec/support/helpers/construct_spec_helper.rb +5 -5
  45. data/spec/support/matchers/be_appropriate_construct_matcher.rb +9 -1
  46. data/spec/support/matchers/have_accessor_matcher.rb +6 -3
  47. metadata +18 -7
  48. data/lib/taketo/config_printer.rb +0 -84
  49. data/lib/taketo/support/eval_delegator.rb +0 -25
  50. data/spec/lib/taketo/config_printer_spec.rb +0 -116
  51. data/spec/lib/taketo/support/eval_delegator_spec.rb +0 -43
@@ -0,0 +1,29 @@
1
+ require 'taketo/constructs'
2
+
3
+ module Taketo
4
+ def self.downcased_construct_class_name(klass)
5
+ klass.name.gsub("Taketo::Constructs::", "").gsub(/[A-Z][^A-Z]*/) { |s| s.gsub("::", "").downcase + "_" }.chop
6
+ end
7
+
8
+ class ConfigVisitor
9
+ include Taketo::Constructs
10
+
11
+ def self.visit(*klasses, &block)
12
+ klasses.each do |klass|
13
+ define_method(:"visit_#{Taketo.downcased_construct_class_name(klass)}", block)
14
+ end
15
+ end
16
+
17
+ def visit(obj)
18
+ obj.class.ancestors.each do |ancestor|
19
+ next unless ancestor.name # skip anonymous classes
20
+ method_name = :"visit_#{Taketo.downcased_construct_class_name(ancestor)}"
21
+ next unless respond_to?(method_name)
22
+ return send(method_name, obj)
23
+ end
24
+
25
+ raise "Don't know how to visit #{obj.class}"
26
+ end
27
+ end
28
+ end
29
+
@@ -1,72 +1,27 @@
1
1
  require 'taketo/support'
2
+ require 'taketo/associated_nodes'
2
3
 
3
4
  module Taketo
4
- class NodesNotDefinedError < StandardError; end
5
-
6
5
  module Constructs
7
6
  class BaseConstruct
8
- attr_reader :name
9
-
10
- ##
11
- # Adds nodes collections to the construct
12
- #
13
- # Example:
14
- #
15
- # class Bar < BaseConstruct
16
- # has_nodes :foos, :foo
17
- # end
18
- #
19
- # bar = Bar.new
20
- # bar.foos # => foos collection
21
- # bar.append_foo(foo) # adds node the collection
22
- # bar.find_foo(:foo_name) # find foo in foos collection by name
23
- #
24
- def self.has_nodes(name_plural, name_singular)
25
- self.node_types << name_plural
26
-
27
- define_method "append_#{name_singular}" do |object|
28
- nodes(name_plural) << object
29
- end
30
7
 
31
- define_method "find_#{name_singular}" do |name|
32
- nodes(name_plural).find_by_name(name)
33
- end
34
-
35
- define_method name_plural do
36
- nodes(name_plural)
37
- end
38
- end
39
-
40
- def self.node_types
41
- @node_types ||= []
42
- end
8
+ include AssociatedNodes
9
+ attr_reader :name
43
10
 
44
11
  def initialize(name)
12
+ super
45
13
  @name = name
46
- @nodes = {}
47
- end
48
-
49
- def find(singular_node_name, name)
50
- send("find_#{singular_node_name}", name) or
51
- if block_given?
52
- yield
53
- else
54
- raise KeyError, "#{singular_node_name} #{name} not found for #{node_type} #{self.name}"
55
- end
56
- end
57
-
58
- def nodes(name_plural)
59
- unless self.class.node_types.include?(name_plural)
60
- raise NodesNotDefinedError, "#{name_plural} not defined for #{node_type}"
61
- end
62
- @nodes[name_plural] ||= Taketo::Support::NamedNodesCollection.new
63
14
  end
64
15
 
65
16
  def node_type
66
17
  demodulized = self.class.name.gsub(/.*::/, '')
67
18
  demodulized.gsub(/([a-z])([A-Z])/, '\\1_\\2').downcase.to_sym
68
19
  end
69
-
20
+
21
+ def qualified_name
22
+ "#{node_type} #{self.name}"
23
+ end
24
+
70
25
  end
71
26
  end
72
27
  end
@@ -7,7 +7,15 @@ module Taketo
7
7
  include Shellwords
8
8
 
9
9
  attr_accessor :command, :description
10
-
10
+
11
+ def self.default
12
+ new(:default).tap { |cmd| cmd.command = "bash" }
13
+ end
14
+
15
+ def self.explicit_command(command_string)
16
+ new(:explicit_command).tap { |cmd| cmd.command = command_string }
17
+ end
18
+
11
19
  def render(server, options = {})
12
20
  %Q[#{location(server, options)} #{environment_variables(server)} #{command}].strip.squeeze(" ")
13
21
  end
@@ -6,9 +6,19 @@ module Taketo
6
6
  class Environment < BaseConstruct
7
7
  has_nodes :servers, :server
8
8
 
9
+ attr_accessor :project
10
+
11
+ def initialize(name)
12
+ super(name)
13
+ end
14
+
15
+ def project_name
16
+ @project.name if defined? @project
17
+ end
18
+
9
19
  def append_server(server)
10
20
  server.environment = self
11
- nodes(:servers) << server
21
+ super
12
22
  end
13
23
  end
14
24
  end
@@ -5,6 +5,11 @@ module Taketo
5
5
  module Constructs
6
6
  class Project < BaseConstruct
7
7
  has_nodes :environments, :environment
8
+
9
+ def append_environment(environment)
10
+ environment.project = self
11
+ super
12
+ end
8
13
  end
9
14
  end
10
15
  end
@@ -1,14 +1,13 @@
1
1
  require 'taketo/constructs/base_construct'
2
+ require 'taketo/constructs/command'
2
3
  require 'taketo/support'
3
4
 
4
5
  module Taketo
5
- class CommandNotFoundError < StandardError; end
6
-
7
6
  module Constructs
8
7
  class Server < BaseConstruct
9
8
  attr_reader :environment_variables
10
- attr_accessor :host, :port, :username, :default_location, :environment, :global_alias, :identity_file
11
-
9
+ attr_accessor :host, :port, :username, :default_location, :default_command, :environment, :global_alias, :identity_file
10
+
12
11
  has_nodes :commands, :command
13
12
 
14
13
  def initialize(name)
@@ -24,6 +23,10 @@ module Taketo
24
23
  env(:RAILS_ENV => environment.name.to_s)
25
24
  @environment = environment
26
25
  end
26
+
27
+ def default_command
28
+ defined?(@default_command) ? @default_command : Command.default
29
+ end
27
30
  end
28
31
  end
29
32
  end
data/lib/taketo/dsl.rb CHANGED
@@ -74,9 +74,20 @@ module Taketo
74
74
  @shared_server_configs.store(name.to_sym, blk)
75
75
  end
76
76
 
77
- define_method_in_scope(:include_shared_server_config, :server) do |name, *args|
78
- instance_exec(*args, &shared_server_configs[name])
77
+ define_method_in_scope(:include_shared_server_config, :server) do |*args|
78
+ if args.first.is_a?(Hash)
79
+ configs_and_arguments = args.shift
80
+ configs_and_arguments.each do |config_name, arguments|
81
+ instance_exec(*arguments, &shared_server_configs[config_name])
82
+ end
83
+ else
84
+ args.each do |config_name|
85
+ instance_exec(&shared_server_configs[config_name])
86
+ end
87
+ end
79
88
  end
89
+ alias :include_shared_server_configs :include_shared_server_config
90
+
80
91
 
81
92
  private
82
93
 
@@ -15,6 +15,7 @@ module Taketo
15
15
 
16
16
  def push(node)
17
17
  @nodes << node unless find_by_name(node.name)
18
+ self
18
19
  end
19
20
  alias :<< :push
20
21
 
@@ -29,6 +30,15 @@ module Taketo
29
30
  def find_by_name(name)
30
31
  @nodes.detect { |n| n.name == name }
31
32
  end
33
+
34
+ def ==(other)
35
+ return true if other.equal?(self)
36
+ @nodes == other.is_a?(NamedNodesCollection) ? other.nodes : other
37
+ end
38
+
39
+ protected
40
+
41
+ attr_reader :nodes
32
42
  end
33
43
  end
34
44
  end
@@ -3,7 +3,6 @@ module Taketo
3
3
  end
4
4
  end
5
5
 
6
- require 'taketo/support/eval_delegator'
7
6
  require 'taketo/support/named_nodes_collection'
8
7
  require 'taketo/support/key_error'
9
8
 
data/lib/taketo.rb CHANGED
@@ -6,6 +6,7 @@ end
6
6
  require 'taketo/support'
7
7
  require 'taketo/dsl'
8
8
  require 'taketo/commands'
9
+ require 'taketo/config_traverser'
9
10
  require 'taketo/config_validator'
10
- require 'taketo/config_printer'
11
+ require 'taketo/config_printer_visitor'
11
12
 
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
  require 'taketo/dsl'
3
3
 
4
4
  describe "Taketo DSL" do
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+ require 'taketo/associated_nodes'
3
+
4
+ include Taketo
5
+
6
+ class TestAssociatedNodes
7
+ include AssociatedNodes
8
+
9
+ has_nodes :foos, :foo
10
+ end
11
+
12
+ describe "AssociatedNodes" do
13
+ subject(:construct) { TestAssociatedNodes.new(:my_node) }
14
+ let(:foo) { stub(:FooNode, :name => :bar) }
15
+
16
+ describe ".has_nodes" do
17
+ it "adds node type to class's node_types" do
18
+ expect(TestAssociatedNodes.node_types).to include(:foos)
19
+ end
20
+
21
+ specify "#foos returns mutable foos collection" do
22
+ construct.nodes(:foos) << foo
23
+ expect(construct.foos).to eq([foo])
24
+ end
25
+
26
+ specify "#append_foo adds a foo to nested foos collection" do
27
+ construct.nodes(:foos) << foo
28
+ expect(construct.nodes(:foos)).to include(foo)
29
+ end
30
+
31
+ specify "#find_foo returns node with corresponding name" do
32
+ construct.nodes(:foos) << foo
33
+ expect(construct.find_foo(:bar)).to eq(foo)
34
+ end
35
+
36
+ describe "#has_foos?" do
37
+ context "when there are no foos" do
38
+ it "returns false" do
39
+ expect(construct).not_to have_foos
40
+ end
41
+ end
42
+
43
+ context "when there are some foos" do
44
+ it "returns true" do
45
+ construct.nodes(:foos) << foo
46
+ expect(construct).to have_foos
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#find" do
53
+ context "when node is present" do
54
+ it "returns node object with corresponding name" do
55
+ construct.nodes(:foos) << foo
56
+ expect(construct.find(:foo, :bar)).to eq(foo)
57
+ end
58
+ end
59
+
60
+ context "when node is absent" do
61
+ it "yields if block given" do
62
+ expect { |b| construct.find(:foo, :bar, &b) }.to yield_control
63
+ end
64
+
65
+ it "raises KeyError if no block given" do
66
+ expect { construct.find(:foo, :bar) }.to raise_error(KeyError)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#nodes" do
72
+ it "returns nodes collection by plural name" do
73
+ construct.nodes(:foos) << foo
74
+ expect(construct.nodes(:foos)).to eq([foo])
75
+ end
76
+
77
+ it "raises NodesNotDefinedError if non-specified node requested" do
78
+ expect { construct.nodes(:bars) }.to raise_error(NodesNotDefinedError, /bars/)
79
+ end
80
+ end
81
+
82
+ describe "#has_nodes?" do
83
+ context "when nodes are present" do
84
+ it "returns true" do
85
+ construct.nodes(:foos) << foo
86
+ expect(construct).to have_nodes(:foos)
87
+ end
88
+ end
89
+
90
+ context "when nodes are absent" do
91
+ it "returns false" do
92
+ expect(construct).not_to have_nodes(:foos)
93
+ end
94
+ end
95
+
96
+ it "raises if node can not have children of specified type" do
97
+ expect { construct.has_nodes?(:quack) }.to raise_error NodesNotDefinedError
98
+ end
99
+ end
100
+ end
101
+
@@ -1,10 +1,9 @@
1
- require File.expand_path('../../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
  require 'taketo/commands/ssh_command'
3
3
 
4
4
  include Taketo::Commands
5
5
 
6
6
  describe "SSH Command" do
7
- let(:environment) { stub(:Environment, :name => :staging) }
8
7
  let(:server) do
9
8
  stub(:Server, :name => :s1,
10
9
  :host => "1.2.3.4",
@@ -12,27 +11,18 @@ describe "SSH Command" do
12
11
  :username => "deployer",
13
12
  :default_location => "/var/app",
14
13
  :identity_file => "/home/gor/.ssh/qqq",
15
- :environment => environment,
16
14
  :environment_variables => {})
17
15
  end
18
- let(:command) { mock(:Command, :render => "the_command") }
19
16
 
20
- let(:ssh_command) { SSHCommand.new(server) }
17
+ subject(:ssh_command) { SSHCommand.new(server) }
21
18
 
22
- it "should compose command based on provided server object" do
19
+ it "composes command based on provided server object" do
23
20
  ssh_command.render("foobar").should == %q[ssh -t -p 22 -i /home/gor/.ssh/qqq deployer@1.2.3.4 "foobar"]
24
21
  end
25
22
 
26
- it "should ignore absent parts" do
27
- server.stub(:port => nil)
28
- server.stub(:username => nil)
29
- server.stub(:identity_file => nil)
23
+ it "ignores absent parts if they are not required" do
24
+ server.stub(:port => nil, :username => nil, :identity_file => nil)
30
25
  ssh_command.render("foobar").should == %q[ssh -t 1.2.3.4 "foobar"]
31
26
  end
32
-
33
- it "should require host" do
34
- server.stub(:host => nil)
35
- expect { ssh_command.render("bash") }.to raise_error ArgumentError, /host/
36
- end
37
27
  end
38
28
 
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+ require 'taketo/config_printer_visitor'
3
+
4
+ include Taketo
5
+
6
+ describe "ConfigPrinterVisitor" do
7
+ subject(:printer) { ConfigPrinterVisitor.new }
8
+
9
+ describe "#visit_command" do
10
+ let(:command) { stub(:Command, :name => :foo, :description => nil) }
11
+
12
+ it "renders command name" do
13
+ printer.visit_command(command)
14
+ expect(printer.result).to eq("foo")
15
+ end
16
+
17
+ it "renders description if available" do
18
+ command.stub(:description => "The description")
19
+ printer.visit_command(command)
20
+ expect(printer.result).to eq("foo - The description")
21
+ end
22
+ end
23
+
24
+ describe "#visit_server" do
25
+ let(:server) do
26
+ stub(:Server,
27
+ :name => :sponge,
28
+ :host => "1.2.3.4",
29
+ :port => 8000,
30
+ :username => "bob",
31
+ :default_location => "/var/app",
32
+ :environment_variables => { :FOO => "bar", :BOO => "baz" })
33
+ end
34
+
35
+ it "renders available server info" do
36
+ server.stub(:has_commands? => true)
37
+ printer.visit_server(server)
38
+ expect(printer.result).to match(
39
+ %r[Server: sponge
40
+ Host: 1\.2\.3\.4
41
+ Port: 8000
42
+ User: bob
43
+ Default location: /var/app
44
+ Environment: (FOO=bar BOO=baz|BOO=baz FOO=bar)])
45
+ end
46
+
47
+ it "renders appropriate message if there are no commands" do
48
+ server.stub(:has_commands? => false)
49
+ printer.visit_server(server)
50
+ expect(printer.result).to include("(No commands)")
51
+ end
52
+ end
53
+
54
+ describe "#visit_environment" do
55
+ let(:environment) { stub(:Environment, :name => :foo) }
56
+
57
+ it "renders environment info" do
58
+ environment.stub(:has_servers? => true)
59
+ printer.visit_environment(environment)
60
+ expect(printer.result).to eq("Environment: foo")
61
+ end
62
+
63
+ it "renders appropriate message if there are no servers" do
64
+ environment.stub(:has_servers? => false)
65
+ printer.visit_environment(environment)
66
+ expect(printer.result).to include("Environment: foo\n (No servers)")
67
+ end
68
+ end
69
+
70
+ describe "#visit_project" do
71
+ let(:project) { stub(:Project, :name => :quux) }
72
+
73
+ it "renders project info" do
74
+ project.stub(:has_environments? => true)
75
+ printer.visit_project(project)
76
+ expect(printer.result).to eq("\nProject: quux")
77
+ end
78
+
79
+ it "renders appropriate message if there are no environments for project" do
80
+ project.stub(:has_environments? => false)
81
+ printer.visit_project(project)
82
+ expect(printer.result).to eq("\nProject: quux\n (No environments)")
83
+ end
84
+ end
85
+
86
+ describe "#visit_config" do
87
+ let(:config) { stub(:Config, :default_destination => "hello:bye") }
88
+
89
+ it "renders default destination and all projects" do
90
+ config.stub(:has_projects? => true)
91
+ printer.visit_config(config)
92
+ expect(printer.result).to eq("Default destination: hello:bye")
93
+ end
94
+
95
+ it "renders appropriate message if there are no projects" do
96
+ config.stub(:has_projects? => false)
97
+ printer.visit_config(config)
98
+ expect(printer.result).to eq("There are no projects yet...")
99
+ end
100
+ end
101
+
102
+ it "indents relatively" do
103
+ config = stub(:Config, :default_destination => "hello:bye", :has_projects? => true)
104
+ project = stub(:Project, :name => :quux, :has_environments? => true)
105
+ environment = stub(:Environment, :name => :foo, :has_servers? => false)
106
+ printer.visit_config(config)
107
+ printer.visit_project(project)
108
+ printer.visit_environment(environment)
109
+ expect(printer.result).to eq("Default destination: hello:bye\n\nProject: quux\n Environment: foo\n (No servers)")
110
+ end
111
+ end
112
+
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'taketo/config_traverser'
3
+
4
+ # config 1
5
+ # / \
6
+ # project 1 2
7
+ # / \
8
+ # environment 1 2
9
+ # /|\
10
+ # server 1 2 3
11
+
12
+ describe Taketo::ConfigTraverser do
13
+ let(:server_1) { stub(:Server1, :node_type => :server, :name => :server_1).as_null_object }
14
+ let(:server_2) { stub(:Server2, :node_type => :server, :name => :server_2).as_null_object }
15
+ let(:server_3) { stub(:Server3, :node_type => :server, :name => :server_3).as_null_object }
16
+
17
+ let(:environment_1) { stub(:Environment1, :node_type => :environment, :name => :environment_1) }
18
+ let(:environment_2) { stub(:Environment2, :node_type => :environment, :name => :environment_2, :servers => [server_1, server_2, server_3]) }
19
+
20
+ let(:project_1) { stub(:Project1, :node_type => :project, :name => :project_1, :environments => [environment_1, environment_2]) }
21
+ let(:project_2) { stub(:Project2, :node_type => :project, :name => :project_2) }
22
+
23
+ let(:config) { stub(:Config, :node_type => :config, :projects => [project_1, project_2], :name => :config) }
24
+
25
+ let(:traverser) { described_class.new(config) }
26
+
27
+ before do
28
+ config.stub(:has_nodes?).with(:projects).and_return(true)
29
+ config.stub(:nodes).with(:projects).and_return([project_1, project_2])
30
+
31
+ project_1.stub(:has_nodes?).with(:environments).and_return(true)
32
+ project_1.stub(:nodes).with(:environments).and_return([environment_1, environment_2])
33
+
34
+ project_2.stub(:has_nodes?).with(:environments).and_return(false)
35
+ project_2.should_not_receive(:nodes)
36
+
37
+ environment_1.stub(:has_nodes?).with(:servers).and_return(false)
38
+ environment_1.should_not_receive(:nodes)
39
+
40
+ environment_2.stub(:has_nodes?).with(:servers).and_return(true)
41
+ environment_2.stub(:nodes).and_return([server_1, server_2, server_3])
42
+ end
43
+
44
+ class PrintingVisitor
45
+ def initialize
46
+ @result = []
47
+ end
48
+
49
+ def visit(type)
50
+ end
51
+ end
52
+
53
+ describe "#visit_depth_first" do
54
+ it "traverses in depth with visitor" do
55
+ visitor = stub(:Visitor)
56
+ [config, project_1, environment_1, environment_2, server_1, server_2, server_3, project_2].each do |node|
57
+ visitor.should_receive(:visit).with(node).ordered
58
+ end
59
+ traverser.visit_depth_first(visitor)
60
+ end
61
+ end
62
+ end
63
+