cult 0.1.1.pre → 0.1.2.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -28
  3. data/cult +1 -1
  4. data/cult.gemspec +4 -4
  5. data/doc/images/masthead@0.5x.png +0 -0
  6. data/exe/cult +2 -5
  7. data/lib/cult/artifact.rb +2 -1
  8. data/lib/cult/bundle.rb +26 -0
  9. data/lib/cult/cli/common.rb +2 -0
  10. data/lib/cult/cli/console_cmd.rb +11 -11
  11. data/lib/cult/cli/cri_extensions.rb +1 -2
  12. data/lib/cult/cli/fleet_cmd.rb +37 -0
  13. data/lib/cult/cli/init_cmd.rb +0 -2
  14. data/lib/cult/cli/node_cmd.rb +54 -22
  15. data/lib/cult/cli/task_cmd.rb +25 -9
  16. data/lib/cult/commander.rb +78 -52
  17. data/lib/cult/definition.rb +4 -7
  18. data/lib/cult/driver.rb +1 -1
  19. data/lib/cult/drivers/common.rb +8 -21
  20. data/lib/cult/drivers/digital_ocean_driver.rb +41 -48
  21. data/lib/cult/drivers/linode_driver.rb +12 -19
  22. data/lib/cult/drivers/load.rb +0 -3
  23. data/lib/cult/drivers/vultr_driver.rb +33 -44
  24. data/lib/cult/named_array.rb +62 -14
  25. data/lib/cult/node.rb +43 -8
  26. data/lib/cult/project.rb +0 -8
  27. data/lib/cult/project_context.rb +23 -0
  28. data/lib/cult/provider.rb +0 -3
  29. data/lib/cult/role.rb +30 -50
  30. data/lib/cult/singleton_instances.rb +43 -0
  31. data/lib/cult/skel.rb +2 -2
  32. data/lib/cult/task.rb +30 -8
  33. data/lib/cult/template.rb +18 -70
  34. data/lib/cult/transaction.rb +44 -0
  35. data/lib/cult/transferable.rb +2 -5
  36. data/lib/cult/user_refinements.rb +65 -0
  37. data/lib/cult/version.rb +1 -1
  38. data/lib/cult.rb +26 -0
  39. data/skel/roles/all/tasks/sync +24 -0
  40. data/skel/roles/bootstrap/files/cult-motd +1 -1
  41. metadata +19 -14
  42. data/lib/cult/config.rb +0 -22
  43. data/lib/cult/drivers/script_driver.rb +0 -27
  44. data/skel/keys/.keep +0 -0
@@ -73,27 +73,36 @@ module Cult
73
73
  private :extract_regexp_options
74
74
 
75
75
 
76
- # Returns all keys that match if method == :select, the first if
77
- # method == :find
78
- def all(key, method = :select)
79
- key = case key
80
- when Integer
81
- # Fallback to default behavior
82
- return super
76
+ # Most of the named-array predicates are meant to be useful for user input
77
+ # or an interactive session. We give special behavior to certain strings
78
+ # the user might enter to convert them to a regexp, etc.
79
+ def expand_predicate(predicate)
80
+ case predicate
83
81
  when String
84
- key[0] == '/' ? build_regexp_from_string(key) : key
82
+ predicate[0] == '/' ? build_regexp_from_string(predicate) : predicate
85
83
  when Regexp, Proc, Range
86
- key
87
- when Symbol
88
- key.to_s
84
+ predicate
85
+ when Symbol, Integer
86
+ ->(v) { predicate.to_s == v.to_s }
89
87
  when NilClass
90
- return nil
88
+ nil
91
89
  else
92
- fail KeyError, "#{key} did not resolve to an object"
90
+ fail KeyError, "Invalid predicate: #{predicate.inspect}"
93
91
  end
92
+ end
93
+ private :expand_predicate
94
+
95
+
96
+ # Returns all keys that match if method == :select, the first if
97
+ # method == :find
98
+ def all(key, method = :select)
99
+ return super if key.is_a?(Integer)
100
+ return nil if key.nil?
101
+
102
+ predicate = expand_predicate(key)
94
103
 
95
104
  send(method) do |v|
96
- key === v.named_array_identifier
105
+ predicate === v.named_array_identifier
97
106
  end
98
107
  end
99
108
 
@@ -125,5 +134,44 @@ module Cult
125
134
  def values
126
135
  self
127
136
  end
137
+
138
+ # Takes a predicate in the form of:
139
+ # key: value
140
+ # And returns all items that both respond_to?(key), and
141
+ # predicate === the result of sending key.
142
+ #
143
+ # Instances can override what predicates mean by defining "names_for_*" to
144
+ # override what is tested.
145
+ #
146
+ # For example, if you have an Object that contains a list of "Foos", but
147
+ # you want to select them by name, you'd do something like:
148
+ #
149
+ # class Object
150
+ # attr_reader :foos # Instances of Foo class
151
+ #
152
+ # def names_for_foos # Now we can select by name
153
+ # foos.map(&:name)
154
+ # end
155
+ # end
156
+ #
157
+ def with(**kw)
158
+ fail ArgumentError, "with requires exactly one predicate" if kw.size != 1
159
+
160
+ method, predicate = kw.first
161
+ predicate = expand_predicate(predicate)
162
+
163
+ select do |candidate|
164
+ method = ["names_for_#{method}", method].find do |m|
165
+ candidate.respond_to?(m)
166
+ end
167
+
168
+ if method
169
+ result = Array(candidate.send(method))
170
+ result.any? {|r| predicate === r }
171
+ end
172
+ end
173
+ end
174
+
175
+ alias_method :where, :with
128
176
  end
129
177
  end
data/lib/cult/node.rb CHANGED
@@ -1,5 +1,7 @@
1
- require 'cult/role'
2
1
  require 'fileutils'
2
+ require 'shellwords'
3
+
4
+ require 'cult/role'
3
5
 
4
6
  module Cult
5
7
  class Node < Role
@@ -10,16 +12,17 @@ module Cult
10
12
  FileUtils.mkdir_p(node.path)
11
13
  File.write(project.dump_name(node.node_path),
12
14
  project.dump_object(data))
15
+
16
+ node.generate_ssh_keys!
17
+
13
18
  return by_name(project, data[:name])
14
19
  end
15
20
 
16
- # These are convenience methods for templates, etc.
17
- # delegate them to the definition.
18
- %i(user host ipv4_public ipv4_private ipv6_public ipv6_private).each do |m|
19
- define_method(m) do
20
- definition[m.to_s]
21
- end
22
- end
21
+ delegate_to_definition :host
22
+ delegate_to_definition :ipv4_public
23
+ delegate_to_definition :ipv4_private
24
+ delegate_to_definition :ipv6_public
25
+ delegate_to_definition :ipv6_private
23
26
 
24
27
 
25
28
  def self.path(project)
@@ -56,7 +59,39 @@ module Cult
56
59
  definition.direct('roles') || super
57
60
  end
58
61
 
62
+ def provider
63
+ project.providers[definition['provider']]
64
+ end
65
+
66
+ def names_for_provider
67
+ [ provider&.name ]
68
+ end
59
69
 
60
70
  alias_method :roles, :parent_roles
71
+
72
+
73
+ def ssh_public_key_file
74
+ File.join(path, 'ssh.pub')
75
+ end
76
+
77
+ def ssh_private_key_file
78
+ File.join(path, 'ssh.key')
79
+ end
80
+
81
+ def generate_ssh_keys!
82
+ esc = ->(s) { Shellwords.escape(s) }
83
+ tmp_public = ssh_private_key_file + '.pub'
84
+
85
+ # Wanted to use -o and -t ecdsa, but Net::SSH still has some
86
+ # issues with ECDSA, and only 4.0 beta supports -o style new keys
87
+ cmd = "ssh-keygen -N '' -t rsa -b 4096 -C #{esc.(name)} " +
88
+ "-f #{esc.(ssh_private_key_file)} && " +
89
+ "mv #{esc.(tmp_public)} #{esc.(ssh_public_key_file)}"
90
+ %x(#{cmd})
91
+ unless $?.success?
92
+ fail "Couldn't generate SSH key, command: #{cmd}"
93
+ end
94
+ end
95
+
61
96
  end
62
97
  end
data/lib/cult/project.rb CHANGED
@@ -3,9 +3,6 @@ require 'shellwords'
3
3
  require 'json'
4
4
  require 'yaml'
5
5
 
6
- require 'cult/config'
7
- require 'cult/role'
8
- require 'cult/provider'
9
6
 
10
7
  module Cult
11
8
  class Project
@@ -16,11 +13,6 @@ module Cult
16
13
 
17
14
  def initialize(path)
18
15
  @path = path
19
-
20
- if Cult.immutable?
21
- self.provider
22
- self.freeze
23
- end
24
16
  end
25
17
 
26
18
 
@@ -0,0 +1,23 @@
1
+ require 'forwardable'
2
+
3
+ module Cult
4
+ class ProjectContext
5
+ extend Forwardable
6
+ def_delegators :project, :methods, :respond_to?, :to_s, :inspect
7
+
8
+ attr_reader :project
9
+
10
+ def initialize(project, **extra)
11
+ @project = project
12
+
13
+ extra.each do |k, v|
14
+ define_singleton_method(k) { v }
15
+ end
16
+ end
17
+
18
+ def method_missing(*args)
19
+ project.send(*args)
20
+ end
21
+
22
+ end
23
+ end
data/lib/cult/provider.rb CHANGED
@@ -1,6 +1,3 @@
1
- require 'cult/named_array'
2
- require 'cult/definition'
3
-
4
1
  require 'forwardable'
5
2
 
6
3
  module Cult
data/lib/cult/role.rb CHANGED
@@ -1,25 +1,24 @@
1
1
  require 'tsort'
2
2
 
3
- require 'cult/task'
4
- require 'cult/artifact'
5
- require 'cult/config'
6
- require 'cult/definition'
7
- require 'cult/named_array'
8
-
9
3
  module Cult
10
4
  class Role
5
+ include SingletonInstances
6
+
7
+ def self.delegate_to_definition(method_name, definition_key = nil)
8
+ definition_key ||= method_name
9
+ define_method(method_name) do
10
+ definition[definition_key.to_s]
11
+ end
12
+ end
13
+
14
+ delegate_to_definition :user
15
+
11
16
  attr_accessor :project
12
17
  attr_accessor :path
13
18
 
14
19
  def initialize(project, path)
15
20
  @project = project
16
21
  @path = path
17
-
18
- if Cult.immutable?
19
- definition
20
- parent_roles
21
- self.freeze
22
- end
23
22
  end
24
23
 
25
24
 
@@ -51,11 +50,7 @@ module Cult
51
50
 
52
51
 
53
52
  def inspect
54
- if Cult.immutable?
55
- "\#<#{self.class.name} id:#{object_id.to_s(36)} #{name.inspect}>"
56
- else
57
- "\#<#{self.class.name} #{name.inspect}>"
58
- end
53
+ "\#<#{self.class.name} id:#{object_id.to_s(36)} #{name.inspect}>"
59
54
  end
60
55
  alias_method :to_s, :inspect
61
56
 
@@ -76,6 +71,16 @@ module Cult
76
71
  end
77
72
 
78
73
 
74
+ def build_tasks
75
+ tasks.select(&:build_task?)
76
+ end
77
+
78
+
79
+ def event_tasks
80
+ tasks.select(&:event_task?)
81
+ end
82
+
83
+
79
84
  def artifacts
80
85
  Artifact.all_for_role(project, self)
81
86
  end
@@ -148,39 +153,6 @@ module Cult
148
153
  end
149
154
 
150
155
 
151
- if Cult.immutable?
152
- def self.cache_get(cls, *args)
153
- @singletons ||= {}
154
- key = [cls, *args]
155
-
156
- if (rval = @singletons[key])
157
- return rval
158
- end
159
-
160
- return nil
161
- end
162
-
163
-
164
- def self.cache_put(obj, *args)
165
- @singletons ||= {}
166
- key = [obj.class, *args]
167
- @singletons[key] = obj
168
- obj
169
- end
170
-
171
-
172
- def self.new(*args)
173
- if (result = cache_get(self, *args))
174
- return result
175
- else
176
- result = super
177
- cache_put(result, *args)
178
- return result
179
- end
180
- end
181
- end
182
-
183
-
184
156
  def self.all(project)
185
157
  all_files(project).map do |filename|
186
158
  new(project, filename).tap do |new_role|
@@ -209,5 +181,13 @@ module Cult
209
181
  ! tree[role].nil?
210
182
  end
211
183
 
184
+ def names_for_role(*a)
185
+ build_order.map(&:name)
186
+ end
187
+
188
+ def names_for_task
189
+ tasks.map(&:name)
190
+ end
191
+
212
192
  end
213
193
  end
@@ -0,0 +1,43 @@
1
+ module Cult
2
+ module SingletonInstances
3
+
4
+ module ClassMethods
5
+
6
+ private
7
+ def singletons
8
+ @singletons ||= {}
9
+ end
10
+
11
+
12
+ def cache_get(cls, *args)
13
+ singletons[[cls, *args]]
14
+ end
15
+
16
+
17
+ def cache_put(obj, *args)
18
+ singletons[[obj.class, *args]] = obj
19
+ end
20
+
21
+
22
+ public
23
+ def new(*args)
24
+ return super unless Cult.singletons?
25
+
26
+ if (result = cache_get(self, *args))
27
+ return result
28
+ end
29
+
30
+ super.tap do |result|
31
+ cache_put(result, *args)
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ def self.included(cls)
38
+ class << cls
39
+ prepend(ClassMethods)
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/cult/skel.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'fileutils'
2
- require 'cult/template'
3
2
 
4
3
  module Cult
5
4
  class Skel
@@ -51,7 +50,8 @@ module Cult
51
50
 
52
51
  dst, data = case src
53
52
  when /\.erb\z/
54
- [ dst.sub(/\.erb\z/, ''), template.process(File.read(src))]
53
+ [ dst.sub(/\.erb\z/, ''),
54
+ template.process(File.read(src), filename: src)]
55
55
  else
56
56
  [ dst, File.read(src) ]
57
57
  end
data/lib/cult/task.rb CHANGED
@@ -1,17 +1,17 @@
1
- require 'cult/transferable'
2
- require 'cult/named_array'
3
-
4
1
  module Cult
5
2
  class Task
6
3
  include Transferable
4
+ include SingletonInstances
7
5
 
8
6
  attr_reader :path
9
7
  attr_reader :role
10
8
  attr_reader :serial
11
9
  attr_reader :name
10
+ attr_reader :type
12
11
 
13
- LEADING_ZEROS = 5
12
+ LEADING_ZEROS = 3
14
13
  BASENAME_RE = /\A(\d{#{LEADING_ZEROS},})-([\w-]+)(\..+)?\z/i
14
+ EVENTS = [:sync]
15
15
 
16
16
 
17
17
  def initialize(role, path)
@@ -19,11 +19,20 @@ module Cult
19
19
  @path = path
20
20
  @basename = File.basename(path)
21
21
 
22
+ unless self.class.valid_task_name?(@basename)
23
+ fail ArgumentError, "invalid task name: #{path}"
24
+ end
25
+
22
26
  if (m = @basename.match(BASENAME_RE))
27
+ @type = :build
23
28
  @serial = m[1].to_i
24
29
  @name = m[2]
30
+ elsif EVENTS.map(&:to_s).include?(@basename)
31
+ @type = :event
32
+ @serial = nil
33
+ @name = @basename
25
34
  else
26
- fail ArgumentError, "invalid task name: #{path}"
35
+ fail "WTF"
27
36
  end
28
37
  end
29
38
 
@@ -39,8 +48,18 @@ module Cult
39
48
  end
40
49
 
41
50
 
51
+ def build_task?
52
+ type == :build
53
+ end
54
+
55
+
56
+ def event_task?
57
+ type != :build
58
+ end
59
+
60
+
42
61
  def inspect
43
- "\#<#{self.class.name} role:#{role&.name.inspect} " +
62
+ "\#<#{self.class.name} type: #{type} role:#{role&.name.inspect} " +
44
63
  "serial:#{serial} name:#{name.inspect}>"
45
64
  end
46
65
  alias_method :to_s, :inspect
@@ -50,15 +69,18 @@ module Cult
50
69
  super | 0100
51
70
  end
52
71
 
72
+ def self.valid_task_name?(basename)
73
+ EVENTS.map(&:to_s).include?(basename) || basename.match(BASENAME_RE)
74
+ end
75
+
53
76
 
54
77
  def self.all_for_role(project, role)
55
78
  Dir.glob(File.join(role.path, "tasks", "*")).map do |filename|
56
- next unless File.basename(filename).match(BASENAME_RE)
79
+ next unless valid_task_name?(File.basename(filename))
57
80
  new(role, filename).tap do |new_task|
58
81
  yield new_task if block_given?
59
82
  end
60
83
  end.compact.to_named_array
61
84
  end
62
-
63
85
  end
64
86
  end
data/lib/cult/template.rb CHANGED
@@ -1,91 +1,39 @@
1
1
  require 'erb'
2
2
  require 'json'
3
+ require 'cult/user_refinements'
3
4
 
4
5
  module Cult
5
6
  class Template
7
+ class Context < ProjectContext
8
+ using ::Cult::UserRefinements
6
9
 
7
- # Alright! We found a use for refinements!
8
- module Refinements
9
- module Util
10
- module_function
11
- def squote(s)
12
- "'" + s.gsub("'", "\\\\\'") + "'"
13
- end
14
-
15
-
16
- def quote(s)
17
- s.to_json
18
- end
19
-
20
-
21
- def slash(s)
22
- Shellwords.escape(s)
23
- end
24
- end
25
-
26
- refine String do
27
- def quote
28
- Util.quote(self)
29
- end
30
- alias_method :q, :quote
31
-
32
-
33
- def squote
34
- Util.squote(self)
35
- end
36
- alias_method :sq, :squote
37
-
38
-
39
- def slash
40
- Util.slash(self)
41
- end
42
- end
43
-
44
- refine Array do
45
- def quote(sep = ' ')
46
- map {|v| Util.quote(v) }.join(sep)
47
- end
48
- alias_method :q, :quote
49
-
50
-
51
- def squote(sep = ' ')
52
- map {|v| Util.squote(v) }.join(sep)
53
- end
54
- alias_method :sq, :squote
55
-
56
-
57
- def slash
58
- map {|v| Util.slash(v) }.join(' ')
59
- end
60
- end
61
- end
62
-
63
- class Context
64
- using Refinements
65
-
66
- def initialize(pwd: nil, **kw)
10
+ def initialize(project, pwd: nil, **kw)
67
11
  @pwd = pwd
68
- kw.each do |k,v|
69
- define_singleton_method(k) { v }
70
- end
12
+ super(project, **kw)
71
13
  end
72
14
 
73
-
74
- def _process(template)
15
+ def _process(template, filename: nil)
75
16
  Dir.chdir(@pwd || Dir.pwd) do
76
- ::ERB.new(template).result(binding)
17
+ erb = ::ERB.new(template)
18
+ erb.filename = filename
19
+ erb.result(binding)
77
20
  end
78
21
  end
22
+
23
+ def binding
24
+ super
25
+ end
79
26
  end
80
27
 
28
+ attr_reader :context
81
29
 
82
- def initialize(pwd: nil, **kw)
83
- @context = Context.new(pwd: pwd, **kw)
30
+ def initialize(project:, pwd: nil, **kw)
31
+ @context = Context.new(project, pwd: pwd, **kw)
84
32
  end
85
33
 
86
34
 
87
- def process(text)
88
- @context._process(text)
35
+ def process(text, filename: nil)
36
+ context._process(text, filename: filename)
89
37
  end
90
38
 
91
39
  end
@@ -0,0 +1,44 @@
1
+ module Cult
2
+ module Transaction
3
+ class Log
4
+ attr_reader :steps
5
+ def initialize
6
+ @steps = []
7
+ yield self if block_given?
8
+ end
9
+
10
+ def unwind
11
+ begin
12
+ while (step = steps.pop)
13
+ step.call
14
+ end
15
+ rescue Exception => e
16
+ puts "Error raised while rolling back: #{e.inspect}\n#{e.backtrace}"
17
+ retry
18
+ end
19
+ end
20
+
21
+ def protect(&block)
22
+ begin
23
+ yield
24
+ rescue Exception
25
+ $stderr.puts "Rolling back actions"
26
+ unwind
27
+ raise
28
+ end
29
+ end
30
+
31
+ def rollback(&block)
32
+ steps.push(block)
33
+ end
34
+ end
35
+
36
+ def transaction
37
+ Log.new do |list|
38
+ list.protect do
39
+ yield list
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,8 +1,5 @@
1
- require 'cult/template'
2
-
3
1
  module Cult
4
2
  module Transferable
5
-
6
3
  module ClassMethods
7
4
  def collection_name
8
5
  name.split('::')[-1].downcase + 's'
@@ -39,8 +36,8 @@ module Cult
39
36
  if binary?
40
37
  File.read(path)
41
38
  else
42
- erb = Template.new(pwd: pwd, project: project, role: role, node: node)
43
- erb.process File.read(path)
39
+ erb = Template.new(project: project, pwd: pwd, role: role, node: node)
40
+ erb.process(File.read(path), filename: path)
44
41
  end
45
42
  end
46
43