taketo 0.0.6 → 0.0.7

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 (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
+