cult 0.1.1.pre
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 +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +240 -0
- data/Rakefile +6 -0
- data/cult +1 -0
- data/cult.gemspec +38 -0
- data/doc/welcome.txt +1 -0
- data/exe/cult +86 -0
- data/lib/cult/artifact.rb +45 -0
- data/lib/cult/cli/common.rb +265 -0
- data/lib/cult/cli/console_cmd.rb +124 -0
- data/lib/cult/cli/cri_extensions.rb +84 -0
- data/lib/cult/cli/init_cmd.rb +116 -0
- data/lib/cult/cli/load.rb +26 -0
- data/lib/cult/cli/node_cmd.rb +205 -0
- data/lib/cult/cli/provider_cmd.rb +123 -0
- data/lib/cult/cli/role_cmd.rb +149 -0
- data/lib/cult/cli/task_cmd.rb +140 -0
- data/lib/cult/commander.rb +103 -0
- data/lib/cult/config.rb +22 -0
- data/lib/cult/definition.rb +112 -0
- data/lib/cult/driver.rb +88 -0
- data/lib/cult/drivers/common.rb +192 -0
- data/lib/cult/drivers/digital_ocean_driver.rb +179 -0
- data/lib/cult/drivers/linode_driver.rb +282 -0
- data/lib/cult/drivers/load.rb +26 -0
- data/lib/cult/drivers/script_driver.rb +27 -0
- data/lib/cult/drivers/vultr_driver.rb +217 -0
- data/lib/cult/named_array.rb +129 -0
- data/lib/cult/node.rb +62 -0
- data/lib/cult/project.rb +169 -0
- data/lib/cult/provider.rb +134 -0
- data/lib/cult/role.rb +213 -0
- data/lib/cult/skel.rb +85 -0
- data/lib/cult/task.rb +64 -0
- data/lib/cult/template.rb +92 -0
- data/lib/cult/transferable.rb +61 -0
- data/lib/cult/version.rb +3 -0
- data/lib/cult.rb +4 -0
- data/skel/.cultconsolerc +4 -0
- data/skel/.cultrc.erb +29 -0
- data/skel/README.md.erb +22 -0
- data/skel/keys/.keep +0 -0
- data/skel/nodes/.keep +0 -0
- data/skel/providers/.keep +0 -0
- data/skel/roles/all/role.json +4 -0
- data/skel/roles/all/tasks/00000-do-something-cool +27 -0
- data/skel/roles/bootstrap/files/cult-motd +45 -0
- data/skel/roles/bootstrap/role.json +4 -0
- data/skel/roles/bootstrap/tasks/00000-set-hostname +22 -0
- data/skel/roles/bootstrap/tasks/00001-add-cult-user +21 -0
- data/skel/roles/bootstrap/tasks/00002-install-cult-motd +9 -0
- metadata +183 -0
data/lib/cult/node.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'cult/role'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
class Node < Role
|
6
|
+
def self.from_data!(project, data)
|
7
|
+
node = by_name(project, data[:name])
|
8
|
+
raise Errno::EEXIST if node.exist?
|
9
|
+
|
10
|
+
FileUtils.mkdir_p(node.path)
|
11
|
+
File.write(project.dump_name(node.node_path),
|
12
|
+
project.dump_object(data))
|
13
|
+
return by_name(project, data[:name])
|
14
|
+
end
|
15
|
+
|
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
|
23
|
+
|
24
|
+
|
25
|
+
def self.path(project)
|
26
|
+
File.join(project.path, 'nodes')
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def node_path
|
31
|
+
File.join(path, 'node')
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def state_path
|
36
|
+
File.join(path, 'state')
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def definition_path
|
41
|
+
[ node_path, state_path ]
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def definition_parameters
|
46
|
+
super.merge(node: self)
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def extra_file
|
51
|
+
File.join(path, 'extra')
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def includes
|
56
|
+
definition.direct('roles') || super
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
alias_method :roles, :parent_roles
|
61
|
+
end
|
62
|
+
end
|
data/lib/cult/project.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'cult/config'
|
7
|
+
require 'cult/role'
|
8
|
+
require 'cult/provider'
|
9
|
+
|
10
|
+
module Cult
|
11
|
+
class Project
|
12
|
+
CULT_RC = '.cultrc'
|
13
|
+
|
14
|
+
attr_reader :path
|
15
|
+
attr_accessor :cult_version
|
16
|
+
|
17
|
+
def initialize(path)
|
18
|
+
@path = path
|
19
|
+
|
20
|
+
if Cult.immutable?
|
21
|
+
self.provider
|
22
|
+
self.freeze
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def name
|
28
|
+
File.basename(path)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def cultrc
|
33
|
+
location_of(CULT_RC)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def execute_cultrc
|
38
|
+
load(cultrc)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def inspect
|
43
|
+
"\#<#{self.class.name} name=#{name.inspect} path=#{path.inspect}>"
|
44
|
+
end
|
45
|
+
alias_method :to_s, :inspect
|
46
|
+
|
47
|
+
|
48
|
+
def location_of(file)
|
49
|
+
File.join(path, file)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def relative_path(obj_path)
|
54
|
+
prefix = "#{path}/"
|
55
|
+
|
56
|
+
if obj_path.start_with?(prefix)
|
57
|
+
return obj_path[prefix.length .. -1]
|
58
|
+
end
|
59
|
+
|
60
|
+
fail ArgumentError, "#{path} isn't in the project"
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def remote_path
|
65
|
+
"cult"
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def constructed?
|
70
|
+
File.exist?(cultrc)
|
71
|
+
end
|
72
|
+
alias_method :exist?, :constructed?
|
73
|
+
|
74
|
+
def nodes
|
75
|
+
@nodes ||= begin
|
76
|
+
Node.all(self)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def roles
|
82
|
+
@roles ||= begin
|
83
|
+
Role.all(self)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def providers
|
89
|
+
@providers ||= begin
|
90
|
+
Cult::Provider.all(self)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_writer :default_provider
|
95
|
+
def default_provider
|
96
|
+
@default_provider ||= providers[0]
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def drivers
|
101
|
+
@drivers ||= begin
|
102
|
+
Cult::Drivers.all
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def self.locate(path)
|
108
|
+
path = File.expand_path(path)
|
109
|
+
loop do
|
110
|
+
return nil if path == '/'
|
111
|
+
|
112
|
+
unless File.directory?(path)
|
113
|
+
path = File.dirname(path)
|
114
|
+
end
|
115
|
+
|
116
|
+
candidate = File.join(path, CULT_RC)
|
117
|
+
return new(path) if File.exist?(candidate)
|
118
|
+
path = File.dirname(path)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def self.from_cwd
|
124
|
+
locate Dir.getwd
|
125
|
+
end
|
126
|
+
|
127
|
+
attr_accessor :git_integration
|
128
|
+
alias_method :git?, :git_integration
|
129
|
+
|
130
|
+
def git_branch
|
131
|
+
res = %x(git -C #{Shellwords.escape(path)} branch --no-color)
|
132
|
+
if res && (m = res.match(/^\* (.*)/))
|
133
|
+
return m[1].chomp
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def dump_yaml?
|
139
|
+
!! (ENV['CULT_DUMP'] || '').match(/^yaml$/i)
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
def dump_object(obj)
|
144
|
+
dump_yaml? ? YAML.dump(obj) : JSON.pretty_generate(obj)
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
def dump_name(basename)
|
149
|
+
basename + (dump_yaml? ? '.yml' : '.json')
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
def env
|
154
|
+
ENV['CULT_ENV'] || begin
|
155
|
+
if git_branch&.match(/\bdev(el(opment)?)?\b/)
|
156
|
+
'development'
|
157
|
+
else
|
158
|
+
'production'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
def development?
|
165
|
+
env == 'development'
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'cult/named_array'
|
2
|
+
require 'cult/definition'
|
3
|
+
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module Cult
|
7
|
+
class Provider
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :driver, :sizes, :images, :zones, :provision!, :destroy!
|
11
|
+
|
12
|
+
attr_reader :project
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
|
16
|
+
def initialize(project, path)
|
17
|
+
@project = project
|
18
|
+
@path = path
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def name
|
23
|
+
File.basename(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
prelude = "#{self.class.name} \"#{name}\""
|
29
|
+
driver_name = driver.class.driver_name
|
30
|
+
driver_string = (driver_name == name) ? '' : " driver=\"#{driver_name}\""
|
31
|
+
"\#<#{prelude}#{driver_string}>"
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def driver
|
36
|
+
@driver ||= begin
|
37
|
+
cls = project.drivers[definition['driver']]
|
38
|
+
cls.new(api_key: definition['api_key'])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def definition
|
44
|
+
@definition ||= Definition.new(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def definition_path
|
49
|
+
[File.join(path, "provider"), File.join(path, "defaults")]
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def definition_parameters
|
54
|
+
{ project: self.project }
|
55
|
+
end
|
56
|
+
|
57
|
+
def definition_parents
|
58
|
+
[]
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Chooses the smallest size setup with Ubuntu > Debian > Redhat,
|
63
|
+
# and a random zone.
|
64
|
+
def self.generate_defaults(definition)
|
65
|
+
definition = JSON.parse(definition.to_json)
|
66
|
+
text_to_mb = ->(text) {
|
67
|
+
multipliers = {
|
68
|
+
mb: 1 ** 1,
|
69
|
+
gb: 1 ** 2,
|
70
|
+
tb: 1 ** 3,
|
71
|
+
pb: 1 ** 4
|
72
|
+
}
|
73
|
+
if (m = text.match(/(\d+)([mgtp]b)/))
|
74
|
+
base = m[1].to_i
|
75
|
+
mul = multipliers.fetch(m[2].to_sym)
|
76
|
+
base * mul
|
77
|
+
else
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
}
|
81
|
+
|
82
|
+
conf = definition['configurations']
|
83
|
+
|
84
|
+
# select the smallest size
|
85
|
+
size = conf['sizes'].map do |size|
|
86
|
+
if mb = text_to_mb.(size)
|
87
|
+
[mb, size]
|
88
|
+
else
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
end.compact.sort_by(&:first).last.last
|
92
|
+
|
93
|
+
image = conf['images'].sort_by do |i|
|
94
|
+
case i
|
95
|
+
when /ubuntu-(\d+)-(\d+)/;
|
96
|
+
10000 + ($1.to_i * 100) + ($2.to_i * 10)
|
97
|
+
when /debian-(\d+)/
|
98
|
+
9000 + ($1.to_i * 100)
|
99
|
+
when /(redhat|centos|fedora)-(\d+)/
|
100
|
+
8000 + ($2.to_i * 100)
|
101
|
+
else
|
102
|
+
1
|
103
|
+
end
|
104
|
+
end.last
|
105
|
+
|
106
|
+
zone = conf['zones'].sample
|
107
|
+
|
108
|
+
{
|
109
|
+
default_size: size,
|
110
|
+
default_zone: zone,
|
111
|
+
default_image: image
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
def self.path(project)
|
117
|
+
project.location_of("providers")
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def self.all_files(project)
|
122
|
+
Dir.glob(File.join(path(project), "*")).select do |file|
|
123
|
+
Dir.exist?(file)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
def self.all(project)
|
129
|
+
all_files(project).map do |filename|
|
130
|
+
new(project, filename)
|
131
|
+
end.to_named_array
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/cult/role.rb
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
require 'cult/task'
|
4
|
+
require 'cult/artifact'
|
5
|
+
require 'cult/config'
|
6
|
+
require 'cult/definition'
|
7
|
+
require 'cult/named_array'
|
8
|
+
|
9
|
+
module Cult
|
10
|
+
class Role
|
11
|
+
attr_accessor :project
|
12
|
+
attr_accessor :path
|
13
|
+
|
14
|
+
def initialize(project, path)
|
15
|
+
@project = project
|
16
|
+
@path = path
|
17
|
+
|
18
|
+
if Cult.immutable?
|
19
|
+
definition
|
20
|
+
parent_roles
|
21
|
+
self.freeze
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def exist?
|
27
|
+
Dir.exist?(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def name
|
32
|
+
File.basename(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def collection_name
|
37
|
+
class_name = self.class.name.split('::')[-1]
|
38
|
+
class_name.downcase + 's'
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def remote_path
|
43
|
+
File.join(project.remote_path, collection_name, name)
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def relative_path(obj_path)
|
48
|
+
fail unless obj_path.start_with?(path)
|
49
|
+
obj_path[path.size + 1 .. -1]
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
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
|
59
|
+
end
|
60
|
+
alias_method :to_s, :inspect
|
61
|
+
|
62
|
+
|
63
|
+
def hash
|
64
|
+
[self.class, project, path].hash
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def ==(rhs)
|
69
|
+
[self.class, project, path] == [rhs.class, rhs.project, rhs.path]
|
70
|
+
end
|
71
|
+
alias_method :eql?, :==
|
72
|
+
|
73
|
+
|
74
|
+
def tasks
|
75
|
+
Task.all_for_role(project, self)
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def artifacts
|
80
|
+
Artifact.all_for_role(project, self)
|
81
|
+
end
|
82
|
+
alias_method :files, :artifacts
|
83
|
+
|
84
|
+
|
85
|
+
def definition
|
86
|
+
@definition ||= Definition.new(self)
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def definition_path
|
91
|
+
File.join(path, "role")
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def definition_parameters
|
96
|
+
{ project: project, role: self }
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def definition_parents
|
101
|
+
parent_roles
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def includes
|
106
|
+
definition.direct('includes') || ['all']
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
def parent_roles
|
111
|
+
Array(includes).map do |name|
|
112
|
+
Role.by_name(project, name)
|
113
|
+
end.to_named_array
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def recursive_parent_roles(seen = [])
|
118
|
+
result = []
|
119
|
+
parent_roles.each do |role|
|
120
|
+
next if seen.include?(role)
|
121
|
+
seen.push(role)
|
122
|
+
result.push(role)
|
123
|
+
result += role.recursive_parent_roles(seen)
|
124
|
+
end
|
125
|
+
result.to_named_array
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
def tree
|
130
|
+
([self] + recursive_parent_roles).to_named_array
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def self.by_name(project, name)
|
135
|
+
new(project, File.join(path(project), name))
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
def self.path(project)
|
140
|
+
File.join(project.path, "roles")
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
def self.all_files(project)
|
145
|
+
Dir.glob(File.join(path(project), "*")).select do |file|
|
146
|
+
Dir.exist?(file)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
|
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
|
+
def self.all(project)
|
185
|
+
all_files(project).map do |filename|
|
186
|
+
new(project, filename).tap do |new_role|
|
187
|
+
yield new_role if block_given?
|
188
|
+
end
|
189
|
+
end.to_named_array
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
def build_order
|
194
|
+
all_items = [self] + parent_roles
|
195
|
+
|
196
|
+
each_node = ->(&block) {
|
197
|
+
all_items.each(&block)
|
198
|
+
}
|
199
|
+
|
200
|
+
each_child = ->(node, &block) {
|
201
|
+
node.parent_roles.each(&block)
|
202
|
+
}
|
203
|
+
|
204
|
+
TSort.tsort(each_node, each_child).to_named_array
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
def has_role?(role)
|
209
|
+
! tree[role].nil?
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
end
|
data/lib/cult/skel.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'cult/template'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
class Skel
|
6
|
+
SKEL_DIR = File.expand_path(File.join(__dir__, '../../skel'))
|
7
|
+
|
8
|
+
attr_reader :project
|
9
|
+
|
10
|
+
def initialize(project)
|
11
|
+
@project = project
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def template
|
16
|
+
@erb ||= Template.new(project: project)
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Skeleton files are files that are copied over for a new project.
|
21
|
+
# We allow template files to live in the skeleton directory too, but
|
22
|
+
# they're not copied over until needed.
|
23
|
+
def skeleton_files
|
24
|
+
Dir.glob(File.join(SKEL_DIR, "**", "{.*,*}")).reject do |fn|
|
25
|
+
fn.match(/template/i)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def template_file(name)
|
31
|
+
File.join(SKEL_DIR, name)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def copy_template(name, dst)
|
36
|
+
src = template_file(name)
|
37
|
+
dst = project.location_of(dst)
|
38
|
+
process_file(src, dst)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def process_file(src, dst = nil)
|
43
|
+
dst ||= begin
|
44
|
+
relative = src.sub(%r/\A#{Regexp.escape(SKEL_DIR)}/, '')
|
45
|
+
project.location_of(relative)
|
46
|
+
end
|
47
|
+
|
48
|
+
if File.directory?(src)
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
dst, data = case src
|
53
|
+
when /\.erb\z/
|
54
|
+
[ dst.sub(/\.erb\z/, ''), template.process(File.read(src))]
|
55
|
+
else
|
56
|
+
[ dst, File.read(src) ]
|
57
|
+
end
|
58
|
+
|
59
|
+
display_name = File.basename(dst) == ".keep" ? File.dirname(dst) : dst
|
60
|
+
|
61
|
+
print " Creating #{display_name}"
|
62
|
+
if File.exist?(dst)
|
63
|
+
puts " exists, skipped."
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
69
|
+
|
70
|
+
File.write(dst, data)
|
71
|
+
File.chmod(File.stat(src).mode, dst)
|
72
|
+
puts
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def copy!
|
77
|
+
puts "Creating project from skeleton..."
|
78
|
+
FileUtils.mkdir_p(project.path)
|
79
|
+
skeleton_files.each do |file|
|
80
|
+
process_file(file)
|
81
|
+
end
|
82
|
+
puts
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/cult/task.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'cult/transferable'
|
2
|
+
require 'cult/named_array'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
class Task
|
6
|
+
include Transferable
|
7
|
+
|
8
|
+
attr_reader :path
|
9
|
+
attr_reader :role
|
10
|
+
attr_reader :serial
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
LEADING_ZEROS = 5
|
14
|
+
BASENAME_RE = /\A(\d{#{LEADING_ZEROS},})-([\w-]+)(\..+)?\z/i
|
15
|
+
|
16
|
+
|
17
|
+
def initialize(role, path)
|
18
|
+
@role = role
|
19
|
+
@path = path
|
20
|
+
@basename = File.basename(path)
|
21
|
+
|
22
|
+
if (m = @basename.match(BASENAME_RE))
|
23
|
+
@serial = m[1].to_i
|
24
|
+
@name = m[2]
|
25
|
+
else
|
26
|
+
fail ArgumentError, "invalid task name: #{path}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def self.from_serial_and_name(role, serial:, name:)
|
32
|
+
basename = sprintf("%0#{LEADING_ZEROS}d-%s", serial, name)
|
33
|
+
new(role, File.join(role.path, collection_name, basename))
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def relative_path
|
38
|
+
File.basename(path)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def inspect
|
43
|
+
"\#<#{self.class.name} role:#{role&.name.inspect} " +
|
44
|
+
"serial:#{serial} name:#{name.inspect}>"
|
45
|
+
end
|
46
|
+
alias_method :to_s, :inspect
|
47
|
+
|
48
|
+
|
49
|
+
def file_mode
|
50
|
+
super | 0100
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def self.all_for_role(project, role)
|
55
|
+
Dir.glob(File.join(role.path, "tasks", "*")).map do |filename|
|
56
|
+
next unless File.basename(filename).match(BASENAME_RE)
|
57
|
+
new(role, filename).tap do |new_task|
|
58
|
+
yield new_task if block_given?
|
59
|
+
end
|
60
|
+
end.compact.to_named_array
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|