rbcm 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/action/action.rb +31 -23
- data/app/action/command.rb +6 -4
- data/app/action/file.rb +12 -8
- data/app/action/list.rb +1 -1
- data/app/cli.rb +15 -12
- data/app/node/file.rb +20 -2
- data/app/node/job.rb +1 -1
- data/app/node/job_search.rb +8 -0
- data/app/node/node.rb +8 -2
- data/app/node/remote.rb +4 -4
- data/app/node/sandbox.rb +76 -42
- data/app/project/addon.rb +27 -0
- data/app/project/capability.rb +8 -2
- data/app/project/definition.rb +4 -4
- data/app/project/file.rb +29 -10
- data/app/project/project.rb +59 -11
- data/app/project/template.rb +50 -0
- data/app/project/template_list.rb +19 -0
- data/app/rbcm.rb +24 -18
- metadata +19 -2
- data/app/node/template.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb945c84ce8cafd1cd3e604049885d5a306cc5ad2c7b7a9b9e3fff3e3a91bd3d
|
4
|
+
data.tar.gz: 8427fb88c352244a6168eec50e234d43bcd1557dcb9281c6883d1c1c92e9bf0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1743ddf1827c4b10afd779ade73711959b6f680c35d35a46287866812023ae9150a0200282ffd591e7167f67abc3507d5259df333da383da7b0ee90b046c1db1
|
7
|
+
data.tar.gz: 705a679fed8af59bf5330d0a285232d49743ecae299572ab859d902ac1878f8d1ad17b9419fa28ead1d7c1d5c64902d6bb7790750fcbb518c6f37adfc6ea23f5
|
data/app/action/action.rb
CHANGED
@@ -1,28 +1,36 @@
|
|
1
1
|
class Action
|
2
2
|
attr_accessor :approved, :applied
|
3
|
-
attr_reader :
|
4
|
-
:
|
5
|
-
:source, :path, :tags
|
3
|
+
attr_reader :triggered_by, :trigger, :chain, :dependencies,
|
4
|
+
:obsolete, :job, :check, :triggered, :result,
|
5
|
+
:source, :path, :line, :state, :tags, :working_dir
|
6
6
|
|
7
|
-
def initialize job:,
|
8
|
-
dependencies: nil,
|
9
|
-
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@triggered = []; @source = source
|
14
|
-
@node = node; @job = job
|
15
|
-
@chain = chain; @capability = chain.last
|
16
|
-
@obsolete = nil; @approved = nil
|
17
|
-
@tags = tags.compact.flatten
|
7
|
+
def initialize job:, params: nil, line: nil, check: nil,
|
8
|
+
dependencies: nil, state:
|
9
|
+
@job = job
|
10
|
+
@triggered = []
|
11
|
+
@obsolete = nil
|
12
|
+
@approved = nil
|
18
13
|
# command specific
|
19
|
-
@line = line
|
14
|
+
@line = line
|
20
15
|
# file specific
|
21
|
-
@
|
16
|
+
@params = params
|
17
|
+
@path = params.first if job.capability.name == :file
|
18
|
+
# extract state
|
19
|
+
[:chain, :trigger, :triggered_by, :check, :source, :tags, :working_dirs].each do |key|
|
20
|
+
instance_variable_set "@#{key}", state[key]
|
21
|
+
end
|
22
|
+
@working_dir = @working_dirs.last
|
23
|
+
@dependencies = [:file] + [dependencies].flatten - [@chain.last]
|
24
|
+
end
|
25
|
+
|
26
|
+
def project_file
|
27
|
+
@chain.reverse.find{ |element|
|
28
|
+
defined?(element.project_file) and element.project_file
|
29
|
+
}.project_file
|
22
30
|
end
|
23
31
|
|
24
32
|
def checkable?
|
25
|
-
|
33
|
+
@check.any? or self.class == Action::File
|
26
34
|
end
|
27
35
|
|
28
36
|
def neccessary?
|
@@ -43,9 +51,9 @@ class Action
|
|
43
51
|
end
|
44
52
|
|
45
53
|
def triggered?
|
46
|
-
triggered_by.empty? or triggered_by.one?
|
47
|
-
@node.triggered.flatten.include? triggered_by
|
48
|
-
|
54
|
+
triggered_by.empty? or triggered_by.one?{ |triggered_by|
|
55
|
+
@job.node.triggered.flatten.include? triggered_by
|
56
|
+
}
|
49
57
|
end
|
50
58
|
|
51
59
|
def applied?
|
@@ -63,11 +71,11 @@ class Action
|
|
63
71
|
|
64
72
|
def approve! input=:y
|
65
73
|
if [:a, :y].include? input
|
66
|
-
@node.files[@path].content = content if self.class == Action::File
|
74
|
+
@job.node.files[@path].content = content if self.class == Action::File
|
67
75
|
@approved = true
|
68
76
|
siblings.each.approve! if input == :a
|
69
|
-
@node.triggered << @trigger
|
70
|
-
@triggered = @trigger.compact - @node.triggered
|
77
|
+
@job.node.triggered << @trigger
|
78
|
+
@triggered = @trigger.compact - @job.node.triggered
|
71
79
|
else
|
72
80
|
@approved = false
|
73
81
|
end
|
data/app/action/command.rb
CHANGED
@@ -4,8 +4,10 @@ class Action::Command < Action
|
|
4
4
|
# determine wether the command is neccessary
|
5
5
|
def check!
|
6
6
|
return if @obsolete != nil
|
7
|
-
if @check
|
8
|
-
@obsolete = @
|
7
|
+
if @check.any?
|
8
|
+
@obsolete = @check.all? do |check|
|
9
|
+
@job.node.remote.execute(check).exitstatus == 0
|
10
|
+
end
|
9
11
|
else
|
10
12
|
@obsolete = false
|
11
13
|
end
|
@@ -13,7 +15,7 @@ class Action::Command < Action
|
|
13
15
|
|
14
16
|
# matching commands on other nodes to be approved at once
|
15
17
|
def siblings
|
16
|
-
@node.rbcm.actions.select{ |action|
|
18
|
+
@job.node.rbcm.actions.select{ |action|
|
17
19
|
action.chain[1..-1] == @chain[1..-1] and action.line == @line
|
18
20
|
} - [self]
|
19
21
|
end
|
@@ -21,6 +23,6 @@ class Action::Command < Action
|
|
21
23
|
# execute the command remote
|
22
24
|
def apply!
|
23
25
|
@applied = true
|
24
|
-
@result = @node.remote.execute(@line)
|
26
|
+
@result = @job.node.remote.execute(@line)
|
25
27
|
end
|
26
28
|
end
|
data/app/action/file.rb
CHANGED
@@ -4,11 +4,11 @@ class Action::File < Action
|
|
4
4
|
|
5
5
|
def check!
|
6
6
|
# compare
|
7
|
-
@node.files[path].content
|
7
|
+
@job.node.files[path].content
|
8
8
|
end
|
9
9
|
|
10
10
|
def obsolete
|
11
|
-
@node.files[path].content.chomp.chomp == content.chomp.chomp
|
11
|
+
@job.node.files[path].content.chomp.chomp == content.chomp.chomp
|
12
12
|
end
|
13
13
|
|
14
14
|
def siblings
|
@@ -17,22 +17,26 @@ class Action::File < Action
|
|
17
17
|
|
18
18
|
def apply!
|
19
19
|
@applied = true
|
20
|
-
@result = @node.remote.execute("echo #{Shellwords.escape content} > #{path}")
|
20
|
+
@result = @job.node.remote.execute("echo #{Shellwords.escape content} > #{path}")
|
21
21
|
end
|
22
22
|
|
23
23
|
def content
|
24
24
|
@content ||= if @params[:content]
|
25
25
|
@params[:content].to_s
|
26
26
|
elsif @params[:template]
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
# @node.rbcm.project.templates.find_for(
|
28
|
+
# @params[:template]
|
29
|
+
# ).render context: @params[:context]
|
30
|
+
# Node::Template.new(
|
31
|
+
# name: @params[:template]
|
32
|
+
# ).render context: @params[:context]
|
33
|
+
project_file.project.templates_.for(self).render(
|
30
34
|
context: @params[:context]
|
31
|
-
)
|
35
|
+
)
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
35
39
|
def same_file
|
36
|
-
@node.actions.file(path) - [self]
|
40
|
+
@job.node.actions.file(path) - [self]
|
37
41
|
end
|
38
42
|
end
|
data/app/action/list.rb
CHANGED
data/app/cli.rb
CHANGED
@@ -38,7 +38,7 @@ class CLI
|
|
38
38
|
|
39
39
|
def check action
|
40
40
|
@action = action
|
41
|
-
render checking: action.check
|
41
|
+
render checking: action.check.join("; ")
|
42
42
|
action.check!
|
43
43
|
end
|
44
44
|
|
@@ -49,10 +49,12 @@ class CLI
|
|
49
49
|
render :command if action.class == Action::Command
|
50
50
|
next if not action.approvable?
|
51
51
|
render :siblings if action.siblings.any?
|
52
|
-
render :source if action.source.any?
|
52
|
+
render :source if action.source.flatten.compact.any?
|
53
53
|
render :diff if action.class == Action::File
|
54
54
|
render :prompt
|
55
|
-
sleep 0.25 unless [:a,:y,:n].include? r = STDIN.getch.to_sym # avoid 'ctrl-c'-trap
|
55
|
+
sleep 0.25 unless [:a,:y,:n,:i].include? r = STDIN.getch.to_sym # avoid 'ctrl-c'-trap
|
56
|
+
(binding.pry; sleep 1) if r == :i
|
57
|
+
(puts; exit) if r == :q
|
56
58
|
action.approve! r
|
57
59
|
render :approved
|
58
60
|
render :triggered if action.triggered.any?
|
@@ -64,7 +66,7 @@ class CLI
|
|
64
66
|
@action = action
|
65
67
|
response = action.apply!
|
66
68
|
render :title, color: response.exitstatus == 0 ? :green : :red
|
67
|
-
render :command if response.exitstatus != 0
|
69
|
+
render :command if action.class == Action::Command and response.exitstatus != 0
|
68
70
|
render response: response if response.length > 0
|
69
71
|
end
|
70
72
|
end
|
@@ -75,22 +77,23 @@ class CLI
|
|
75
77
|
out "#{first ? nil : "┗━━──"}\n\n┏━━#{format :invert, :bold}#{" "*16}#{section}#{" "*16}#{format}━──\n┃"
|
76
78
|
elsif element == :title
|
77
79
|
triggerd_by = "#{format :trigger, :bold} #{@action.triggered_by.join(", ")} " if @action.triggered_by.any?
|
78
|
-
out "┣━ #{triggerd_by}#{format color, :bold} #{
|
80
|
+
out "┣━ #{triggerd_by}#{format color, :bold} #{@action.chain.flatten.compact.join(" > ")} " +
|
79
81
|
"#{format} #{format :params}#{@action.job.params if @action.job}#{format}" +
|
80
82
|
" #{format :tag}#{"tags: " if @action.tags.any?}#{@action.tags.join(", ")}#{format}"
|
81
83
|
elsif element == :capabilities
|
82
84
|
out prefix + "capabilities: #{Node::Sandbox.capabilities.join(", ")}"
|
83
85
|
elsif element == :project
|
84
|
-
out prefix + "project: #{@rbcm.project.files.count} ruby files"
|
86
|
+
out prefix + "project: #{@rbcm.project.files.count} ruby files, #{@rbcm.project.templates.count} templates"
|
87
|
+
out prefix + " #{@rbcm.project.directories.count} directories, #{@rbcm.project.other.count} other files"
|
85
88
|
elsif element == :nodes
|
86
89
|
out prefix + @rbcm.nodes.values.collect{ |node|
|
87
|
-
name = node.name.+(":").ljust(@rbcm.nodes.keys.each.length.max+1, " ")
|
90
|
+
name = node.name.to_s.+(":").ljust(@rbcm.nodes.keys.each.length.max+1, " ")
|
88
91
|
jobs = node.jobs.count.to_s.rjust(@rbcm.nodes.values.collect{|node| node.jobs.count}.max.digits.count, " ")
|
89
92
|
actions = node.actions.count.to_s.rjust(@rbcm.nodes.values.collect{|node| node.actions.count}.max.digits.count, " ")
|
90
93
|
"#{name} #{jobs} jobs, #{actions} actions"
|
91
94
|
}.flatten(1).join("\n#{prefix}")
|
92
95
|
elsif element == :command
|
93
|
-
check_string = " UNLESS #{@action.check}" if @action.check
|
96
|
+
check_string = " UNLESS #{@action.check.join("; ")}" if @action.check.any?
|
94
97
|
out prefix + "$> #{@action.line}\e[2m#{check_string}\e[0m"
|
95
98
|
elsif element == :siblings
|
96
99
|
string = @action.siblings.collect do |sibling|
|
@@ -98,17 +101,17 @@ class CLI
|
|
98
101
|
end.join
|
99
102
|
out prefix + "#{format :siblings}siblings:#{format} #{string}"
|
100
103
|
elsif element == :source
|
101
|
-
out prefix + "source: #{format :bold}#{@source.join("#{format}, #{format :bold}")}#{format}"
|
104
|
+
out prefix + "source: #{format :bold}#{@action.source.join("#{format}, #{format :bold}")}#{format}"
|
102
105
|
elsif element == :prompt
|
103
106
|
color = @action.siblings.any? ? :siblings : :light
|
104
|
-
print prefix + "APPROVE? #{format color}[a]ll#{format}, [y]es, [N]o: "
|
107
|
+
print prefix + "APPROVE? #{format color}[a]ll#{format}, [y]es, [N]o, [i]nteractive, [q]uit: "
|
105
108
|
elsif element == :triggered
|
106
109
|
out prefix +
|
107
110
|
"triggered: #{format :trigger} #{@action.triggered.join(", ")} \e[0m;" +
|
108
111
|
" again: #{@action.trigger.-(@action.triggered).join(", ")}"
|
109
112
|
elsif element == :diff
|
110
113
|
out prefix[0..-2] + Diffy::Diff.new(
|
111
|
-
@action.node.files[@action.path].content,
|
114
|
+
@action.job.node.files[@action.path].content,
|
112
115
|
@action.content
|
113
116
|
).to_s(:color).split("\n").join("\n#{prefix[0..-2]}")
|
114
117
|
elsif element == :approved
|
@@ -117,7 +120,7 @@ class CLI
|
|
117
120
|
elsif element.class == String
|
118
121
|
out prefix + "#{element}"
|
119
122
|
elsif checking
|
120
|
-
out prefix + "CHECKING #{@action.node.name}: #{checking}"
|
123
|
+
out prefix + "CHECKING #{@action.job.node.name}: #{checking}"
|
121
124
|
elsif response
|
122
125
|
out prefix + response.to_s.chomp.split("\n").join("\n#{prefix}")
|
123
126
|
elsif element == :applied
|
data/app/node/file.rb
CHANGED
@@ -4,7 +4,7 @@ class Node::NodeFile
|
|
4
4
|
@filesystem = filesystem
|
5
5
|
end
|
6
6
|
|
7
|
-
attr_writer :content, :mode
|
7
|
+
attr_writer :content, :user, :group, :mode
|
8
8
|
|
9
9
|
def content
|
10
10
|
@content ||= (
|
@@ -15,9 +15,27 @@ class Node::NodeFile
|
|
15
15
|
)
|
16
16
|
end
|
17
17
|
|
18
|
+
def diffable # TODO?
|
19
|
+
"#{content}" +
|
20
|
+
"\\" +
|
21
|
+
"PERMISSIONS #{user}:#{group} #{mode}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def user
|
25
|
+
@user ||= @filesystem.node.remote.execute(
|
26
|
+
"stat -c '%U' '#{@path}'"
|
27
|
+
).chomp.chomp
|
28
|
+
end
|
29
|
+
|
30
|
+
def group
|
31
|
+
@group ||= @filesystem.node.remote.execute(
|
32
|
+
"stat -c '%G' '#{@path}'"
|
33
|
+
).chomp.chomp
|
34
|
+
end
|
35
|
+
|
18
36
|
def mode
|
19
37
|
@mode ||= @filesystem.node.remote.execute(
|
20
|
-
"stat -c
|
38
|
+
"stat -c '%a' * '#{@path}'"
|
21
39
|
).chomp.chomp.to_i
|
22
40
|
end
|
23
41
|
end
|
data/app/node/job.rb
CHANGED
data/app/node/node.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
class Node
|
2
|
-
attr_reader :jobs, :definitions, :files, :name, :remote, :rbcm, :sandbox
|
2
|
+
attr_reader :jobs, :definitions, :files, :name, :remote, :rbcm, :sandbox,
|
3
|
+
:path
|
3
4
|
attr_accessor :actions, :memberships, :triggered
|
4
5
|
|
5
|
-
def initialize rbcm, name
|
6
|
+
def initialize rbcm, name, path
|
6
7
|
@rbcm = rbcm
|
7
8
|
@name = name
|
9
|
+
@path = path
|
8
10
|
@definitions = []
|
9
11
|
@sandbox = Node::Sandbox.new self
|
10
12
|
@remote = Node::Remote.new self
|
@@ -33,4 +35,8 @@ class Node
|
|
33
35
|
memberships.include? group
|
34
36
|
}.values.flatten(1)
|
35
37
|
end
|
38
|
+
|
39
|
+
def to_str
|
40
|
+
name.to_s
|
41
|
+
end
|
36
42
|
end
|
data/app/node/remote.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
class Node::Remote
|
2
|
-
attr_reader :files
|
3
|
-
|
4
2
|
def initialize node
|
5
|
-
@
|
3
|
+
@node = node
|
6
4
|
@files = Node::NodeFilesystem.new node
|
7
5
|
end
|
8
6
|
|
7
|
+
attr_reader :node, :files
|
8
|
+
|
9
9
|
def execute action
|
10
|
-
@session ||= Net::SSH.start @
|
10
|
+
@session ||= Net::SSH.start @node.name, 'root'
|
11
11
|
@session.exec! action
|
12
12
|
end
|
13
13
|
end
|
data/app/node/sandbox.rb
CHANGED
@@ -9,8 +9,8 @@ class Node::Sandbox
|
|
9
9
|
@name = node.name
|
10
10
|
@dependency_cache = []
|
11
11
|
@cache = {
|
12
|
-
chain: [@node
|
13
|
-
source: [],
|
12
|
+
chain: [@node], trigger: [], triggered_by: [], check: [],
|
13
|
+
source: [], tags: [], working_dirs: []
|
14
14
|
}
|
15
15
|
# define in instance, otherwise method-binding will be wrong (to class)
|
16
16
|
@@capabilities = @node.rbcm.project.capabilities.each.name
|
@@ -21,21 +21,22 @@ class Node::Sandbox
|
|
21
21
|
[:file, :run].each do |base_capability|
|
22
22
|
__add_capability Project::Capability.new(
|
23
23
|
name: base_capability,
|
24
|
-
content: method(base_capability).unbind
|
24
|
+
content: method(base_capability).unbind,
|
25
|
+
project_file: false
|
25
26
|
)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
29
30
|
def evaluate definitions
|
30
31
|
[definitions].flatten.each do |definition|
|
31
|
-
__cache chain: definition
|
32
|
+
__cache chain: definition do
|
32
33
|
instance_eval &definition.content
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
38
|
def tag name, &block
|
38
|
-
__cache
|
39
|
+
__cache tags: name, chain: "tag:#{name}" do
|
39
40
|
instance_eval &block
|
40
41
|
end
|
41
42
|
end
|
@@ -81,36 +82,59 @@ class Node::Sandbox
|
|
81
82
|
end
|
82
83
|
|
83
84
|
def run action, check: nil, tags: nil, trigger: nil, triggered_by: nil
|
84
|
-
|
85
|
-
node
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
85
|
+
__cache check: check, tags: tags, trigger: trigger, triggered_by: triggered_by, working_dirs: working_dir do
|
86
|
+
@node.actions << Action::Command.new(
|
87
|
+
job: @node.jobs.last,
|
88
|
+
line: action,
|
89
|
+
dependencies: @dependency_cache.dup,
|
90
|
+
state: @cache.collect{|k,v| [k, v.dup]}.to_h,
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def file path, tags: nil, trigger: nil, triggered_by: nil, **named
|
96
|
+
raise "RBCM: invalid file paramteres '#{named}'" if (
|
97
|
+
named.keys - [:exists, :includes_line, :after, :mode, :content,
|
98
|
+
:template, :context, :tags, :user, :group]
|
99
|
+
).any?
|
100
|
+
job = @node.jobs.last
|
101
|
+
run "mkdir -p #{File.dirname path}",
|
102
|
+
check: "ls #{File.dirname path}"
|
103
|
+
__cache tags: tags, trigger: trigger, triggered_by: triggered_by, working_dirs: working_dir do
|
104
|
+
@node.actions << Action::File.new(
|
105
|
+
job: job,
|
106
|
+
params: Params.new([path], named),
|
107
|
+
state: @cache.collect{|k,v| [k, v.dup]}.to_h
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def dir path="", templates:, context: {}, tags: nil, trigger: nil, triggered_by: nil
|
113
|
+
__cache tags: tags, trigger: trigger, triggered_by: triggered_by, working_dirs: working_dir do
|
114
|
+
@node.rbcm.project.templates_.under("#{working_dir}/#{templates}").each do |template|
|
115
|
+
# binding.pry
|
116
|
+
# sleep 1
|
117
|
+
file template.clean_full_path.gsub(/#{working_dir}/),
|
118
|
+
template: template.clean_path,
|
119
|
+
context: context
|
120
|
+
end
|
121
|
+
|
122
|
+
# @node.rbcm.project.templates.select{ |template|
|
123
|
+
# /^#{working_dir}/.match? template
|
124
|
+
# }.each do |template|
|
125
|
+
# file path + template.gsub(/^#{working_dir}\/#{templates}/,"").gsub(".erb", "").gsub(".mustache", ""),
|
126
|
+
# template: template,
|
127
|
+
# context: context
|
128
|
+
# end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def working_dir
|
133
|
+
@cache[:chain].select{ |i|
|
134
|
+
i.class == Project::Definition or (
|
135
|
+
i.class == Project::Capability and not [:file, :run].include? i.name
|
136
|
+
)
|
137
|
+
}.last.project_file.path.split("/")[0..-2].join("/")
|
114
138
|
end
|
115
139
|
|
116
140
|
def decrypt secret
|
@@ -164,24 +188,27 @@ class Node::Sandbox
|
|
164
188
|
end
|
165
189
|
return r.collect &block if block_given? # no-each-syntax
|
166
190
|
return r
|
191
|
+
JobSearch.new r
|
167
192
|
end
|
168
193
|
|
169
194
|
def __cache trigger: nil, triggered_by: nil, params: nil, check: nil,
|
170
|
-
chain:
|
195
|
+
chain: nil, source: nil, reset: nil, tags: nil, working_dirs: nil
|
171
196
|
@cache[:source].append [] if chain
|
172
197
|
@cache[:source].last << source if source
|
173
198
|
@cache[:chain] << chain if chain
|
174
|
-
@cache[:
|
199
|
+
@cache[:tags] << tags if tags
|
175
200
|
@cache[:trigger] << trigger if trigger
|
176
201
|
@cache[:triggered_by] << triggered_by if triggered_by
|
177
202
|
@cache[:check] << check if check
|
203
|
+
@cache[:working_dirs] << working_dirs if working_dirs
|
178
204
|
r = yield if block_given?
|
179
205
|
@cache[:source].pop if chain
|
180
206
|
@cache[:chain].pop if chain
|
181
|
-
@cache[:
|
207
|
+
@cache[:tags].pop if tags
|
182
208
|
@cache[:trigger].pop if trigger
|
183
209
|
@cache[:triggered_by].pop if triggered_by
|
184
210
|
@cache[:check].pop if check
|
211
|
+
@cache[:working_dirs].pop if working_dirs
|
185
212
|
@cache[reset] = [] if reset
|
186
213
|
r
|
187
214
|
end
|
@@ -194,25 +221,32 @@ class Node::Sandbox
|
|
194
221
|
if capability.type == :regular
|
195
222
|
define_singleton_method capability.name do |*ordered, **named|
|
196
223
|
params = Params.new ordered, named
|
197
|
-
@node.jobs.append Node::Job.new
|
224
|
+
@node.jobs.append Node::Job.new(
|
225
|
+
node: @node,
|
226
|
+
capability: capability,
|
227
|
+
params: params
|
228
|
+
)
|
198
229
|
@node.triggered.append capability.name
|
199
230
|
r = __cache trigger: params[:trigger],
|
200
231
|
triggered_by: params[:triggered_by],
|
201
|
-
chain: capability
|
232
|
+
chain: capability do
|
202
233
|
send "__#{__method__}", *params.delete(:trigger, :triggered_by).sendable
|
203
234
|
end
|
204
235
|
@dependency_cache = [:file]
|
205
236
|
r
|
206
237
|
end
|
207
|
-
|
238
|
+
elsif capability.type == :final
|
208
239
|
define_singleton_method capability.name do
|
209
|
-
r = __cache chain:
|
240
|
+
r = __cache chain: capability do
|
210
241
|
send "__#{__method__}"
|
211
242
|
end
|
212
243
|
@dependency_cache = [:file]
|
213
244
|
r
|
214
245
|
end
|
246
|
+
else
|
247
|
+
raise "unknown capability type #{capability.type}"
|
215
248
|
end
|
249
|
+
# return JobSearch.new r
|
216
250
|
end
|
217
251
|
|
218
252
|
def self.capabilities
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Addon < Project
|
2
|
+
def initialize type:, name:
|
3
|
+
@type, @name = type, name
|
4
|
+
if [:file, :dir].include? type
|
5
|
+
super name
|
6
|
+
elsif type == :github
|
7
|
+
super load_from_github name
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :project, :type, :name, :repo
|
12
|
+
|
13
|
+
def load_from_github repo
|
14
|
+
addon_dir = "/tmp/rbcm-addons/"
|
15
|
+
dir = "#{addon_dir}#{repo}"
|
16
|
+
repo = if Dir.exist? dir
|
17
|
+
Git.open dir
|
18
|
+
else
|
19
|
+
Git.clone "https://github.com/#{repo}.git",
|
20
|
+
repo,
|
21
|
+
path: addon_dir
|
22
|
+
end
|
23
|
+
repo.checkout "master"
|
24
|
+
repo.pull
|
25
|
+
return dir
|
26
|
+
end
|
27
|
+
end
|
data/app/project/capability.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
1
|
# holds a capability in form of an unbound method, extracted from
|
2
2
|
# Project::Sandbox module
|
3
|
+
# type - regular: 'cap', final: 'cap!'
|
3
4
|
|
4
5
|
class Project::Capability
|
5
|
-
def initialize name:, content:
|
6
|
+
def initialize name:, content:, project_file:
|
6
7
|
@name = name
|
7
8
|
@content = content
|
8
9
|
@type = type
|
10
|
+
@project_file = project_file
|
9
11
|
end
|
10
12
|
|
11
|
-
attr_reader :name, :content
|
13
|
+
attr_reader :name, :content, :project_file
|
12
14
|
|
13
15
|
def type
|
14
16
|
@name[-1] == "!" ? :final : :regular
|
15
17
|
end
|
18
|
+
|
19
|
+
def to_str
|
20
|
+
name.to_s
|
21
|
+
end
|
16
22
|
end
|
data/app/project/definition.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# holds a definition on form of a proc to be executed in a nodes sandbox
|
2
2
|
|
3
3
|
class Project::Definition
|
4
|
-
def initialize type:, name:, content:
|
5
|
-
@type, @name, @content = type, name, content
|
4
|
+
def initialize type:, name:, content:, project_file:
|
5
|
+
@type, @name, @content, @project_file = type, name, content, project_file
|
6
6
|
end
|
7
7
|
|
8
|
-
attr_reader :type, :name, :content
|
8
|
+
attr_reader :type, :name, :content, :project_file
|
9
9
|
|
10
|
-
def
|
10
|
+
def to_str
|
11
11
|
"#{type}:#{name}"
|
12
12
|
end
|
13
13
|
end
|
data/app/project/file.rb
CHANGED
@@ -1,42 +1,61 @@
|
|
1
1
|
# extracts capabilities and definitions from project files
|
2
2
|
|
3
3
|
class Project::ProjectFile
|
4
|
-
def initialize
|
5
|
-
@
|
4
|
+
def initialize project:, path:
|
5
|
+
@project = project
|
6
|
+
@path = path
|
6
7
|
@definitions = []
|
7
8
|
@capabilities = []
|
8
|
-
|
9
|
+
@addons = []
|
10
|
+
file = File.read path
|
9
11
|
method_names_cache = methods(false)
|
10
12
|
instance_eval file
|
11
13
|
sandbox = Project::Sandbox.dup
|
12
14
|
sandbox.module_eval(file)
|
13
15
|
sandbox.instance_methods.each do |name|
|
16
|
+
raise "ERROR: capability name '#{name}' not allowed" if [:node, :group].include? name
|
14
17
|
@capabilities.append Project::Capability.new(
|
15
|
-
name:
|
16
|
-
content:
|
18
|
+
name: name,
|
19
|
+
content: sandbox.instance_method(name),
|
20
|
+
project_file: self
|
17
21
|
)
|
18
22
|
end
|
19
23
|
end
|
20
24
|
|
21
|
-
attr_reader :capabilities, :definitions, :path
|
25
|
+
attr_reader :project, :capabilities, :definitions, :addon_names, :path, :addons
|
22
26
|
|
23
27
|
private
|
24
28
|
|
29
|
+
def addon branch: "master", **named
|
30
|
+
raise "illegal project source: #{keys}" if (
|
31
|
+
keys = named.keys - [:github, :dir, :file]
|
32
|
+
).any?
|
33
|
+
named.each do |type, name|
|
34
|
+
@addons.append Addon.new type: type, name: name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
25
38
|
def group name=nil
|
26
39
|
@definitions.append Project::Definition.new(
|
27
40
|
type: :group,
|
28
41
|
name: name,
|
29
|
-
content: Proc.new
|
42
|
+
content: Proc.new,
|
43
|
+
project_file: self
|
30
44
|
)
|
31
45
|
end
|
32
46
|
|
33
47
|
def node names=nil
|
34
48
|
[names].flatten(1).each do |name|
|
35
49
|
@definitions.append Project::Definition.new(
|
36
|
-
type:
|
37
|
-
name:
|
38
|
-
content:
|
50
|
+
type: name.class == Regexp ? :pattern : :node,
|
51
|
+
name: name,
|
52
|
+
content: Proc.new,
|
53
|
+
project_file: self
|
39
54
|
)
|
40
55
|
end
|
41
56
|
end
|
57
|
+
|
58
|
+
def relative_path
|
59
|
+
@path.gsub /^#{@project.path}/, ""
|
60
|
+
end
|
42
61
|
end
|
data/app/project/project.rb
CHANGED
@@ -1,25 +1,73 @@
|
|
1
1
|
class Project
|
2
|
-
def initialize path
|
2
|
+
def initialize path, template_engines: [:mustache, :erb], addon: false
|
3
3
|
@path = path
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
@files = []
|
5
|
+
@templates = []
|
6
|
+
@templates_ = Project::TemplateList.new
|
7
|
+
@other = []
|
8
|
+
@directories = []
|
9
|
+
@template_engines = template_engines
|
10
|
+
load_files path
|
11
11
|
end
|
12
12
|
|
13
|
-
attr_reader :path, :files
|
13
|
+
attr_reader :path, :files, :templates, :other, :directories, :templates_
|
14
14
|
|
15
15
|
def capabilities
|
16
|
-
|
16
|
+
files.each.capabilities.flatten.compact
|
17
17
|
end
|
18
18
|
|
19
19
|
def definitions type=nil
|
20
|
-
with
|
20
|
+
with files.each.definitions.flatten do
|
21
21
|
return select{|definition| definition.type == type} if type
|
22
22
|
return self
|
23
23
|
end
|
24
24
|
end
|
25
|
+
|
26
|
+
def files
|
27
|
+
(@files + all_addons.each.files).flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
def addons
|
31
|
+
@files.each.addons.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
# collect addons recursively
|
35
|
+
def all_addons project=self
|
36
|
+
( project.addons + project.addons.collect{|project| all_addons project}
|
37
|
+
).flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
#TODO?
|
41
|
+
def template name
|
42
|
+
# @templates.find{|name| name...}
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def load_files path
|
48
|
+
if File.directory? path
|
49
|
+
Dir["#{path}/**/*"].each do |file_path|
|
50
|
+
if file_path.end_with? ".rb"
|
51
|
+
@files.append Project::ProjectFile.new(
|
52
|
+
project: self,
|
53
|
+
path: file_path
|
54
|
+
)
|
55
|
+
elsif @template_engines.include? file_path.split(".").last.to_sym
|
56
|
+
@templates << file_path.sub(@path, "")
|
57
|
+
@templates_.append Project::Template.new(
|
58
|
+
project: self,
|
59
|
+
path: file_path
|
60
|
+
)
|
61
|
+
elsif File.directory? path
|
62
|
+
@directories << file_path.sub(@path, "")
|
63
|
+
else
|
64
|
+
@other << file_path.sub(@path, "")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
log "templates: #{@templates}"
|
68
|
+
else
|
69
|
+
@files = [Project::ProjectFile.new(@path)]
|
70
|
+
end
|
71
|
+
raise "ERROR: empty project" unless @files.any?
|
72
|
+
end
|
25
73
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Project::Template
|
2
|
+
@@engines = [:erb, :mustache]
|
3
|
+
|
4
|
+
def initialize project:, path:
|
5
|
+
@project = project
|
6
|
+
@path = path
|
7
|
+
@content = File.read path
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :project, :path
|
11
|
+
|
12
|
+
def render context: {}
|
13
|
+
cache = @content
|
14
|
+
engine_names.each do |layer|
|
15
|
+
if layer == :mustache
|
16
|
+
require "mustache"
|
17
|
+
cache = Mustache.render(@content, **context)
|
18
|
+
elsif layer == :erb
|
19
|
+
# https://zaiste.net/rendering_erb_template_with_bindings_from_hash/
|
20
|
+
require "ostruct"; require "erb"
|
21
|
+
cache = ERB.new(@content).result(
|
22
|
+
OpenStruct.new(context).instance_eval{binding}
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
return cache
|
27
|
+
end
|
28
|
+
|
29
|
+
def clean_full_path
|
30
|
+
path.gsub(
|
31
|
+
/#{engine_names.reverse.collect{|e| ".#{e}"}.join}$/, ''
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def clean_path
|
36
|
+
path.gsub(/^#{project.path}/, '').gsub(
|
37
|
+
/#{engine_names.reverse.collect{|e| ".#{e}"}.join}$/, ''
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def clean_filename
|
42
|
+
File.basename(clean_path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def engine_names
|
46
|
+
path.split(".").reverse.collect{ |layer|
|
47
|
+
layer.to_sym if @@engines.include? layer.to_sym
|
48
|
+
}.compact
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Project::TemplateList < Array
|
2
|
+
def for file_action
|
3
|
+
template_name = file_action.job.params[:template]
|
4
|
+
if template_name.start_with? "/" # `/template.txt`
|
5
|
+
find{|template| template.clean_path == template_name}
|
6
|
+
else # `template.txt`
|
7
|
+
find{ |template|
|
8
|
+
template.path.start_with? File.dirname(file_action.project_file.path) and
|
9
|
+
template.clean_filename == template_name
|
10
|
+
}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def under path
|
15
|
+
find_all{ |template|
|
16
|
+
template.path.start_with? path
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
data/app/rbcm.rb
CHANGED
@@ -2,20 +2,23 @@ require "net/ssh"
|
|
2
2
|
require "fileutils"
|
3
3
|
require "shellwords"
|
4
4
|
require "diffy"
|
5
|
+
require "pry"
|
6
|
+
require "git"
|
5
7
|
|
6
8
|
APPDIR = File.expand_path File.dirname(__FILE__)
|
7
|
-
[ "action/action",
|
8
|
-
"action/file",
|
9
|
-
"node/node",
|
10
|
-
"node/job",
|
11
|
-
"node/remote",
|
12
|
-
"node/
|
13
|
-
"lib/lib",
|
14
|
-
"lib/options",
|
15
|
-
"lib/params",
|
16
|
-
"project/project",
|
17
|
-
"project/file",
|
18
|
-
"project/sandbox",
|
9
|
+
[ "action/action", "action/command",
|
10
|
+
"action/file", "action/list",
|
11
|
+
"node/node", "node/file",
|
12
|
+
"node/job", "node/filesystem",
|
13
|
+
"node/remote", "node/sandbox",
|
14
|
+
"node/job_search",
|
15
|
+
"lib/lib", "lib/array_hash",
|
16
|
+
"lib/options", "lib/quick_each",
|
17
|
+
"lib/params", "lib/aescrypt",
|
18
|
+
"project/project", "project/definition",
|
19
|
+
"project/file", "project/capability",
|
20
|
+
"project/sandbox", "project/addon",
|
21
|
+
"project/template", "project/template_list",
|
19
22
|
"cli"
|
20
23
|
].each{|requirement| require "#{APPDIR}/#{requirement}.rb"}
|
21
24
|
|
@@ -27,7 +30,11 @@ class RBCM
|
|
27
30
|
@group_additions = ArrayHash.new
|
28
31
|
@nodes = {}
|
29
32
|
@project.definitions(:node).each do |node_definition|
|
30
|
-
@nodes[node_definition.name] ||= Node.new
|
33
|
+
@nodes[node_definition.name] ||= Node.new(
|
34
|
+
self,
|
35
|
+
node_definition.name,
|
36
|
+
node_definition.project_file.path
|
37
|
+
)
|
31
38
|
@nodes[node_definition.name] << node_definition
|
32
39
|
# apply pattern definitions to node
|
33
40
|
@nodes[node_definition.name] << @project.definitions(:pattern).collect do |pattern_definition|
|
@@ -39,9 +46,6 @@ class RBCM
|
|
39
46
|
@project.definitions(:group).each do |group_definition|
|
40
47
|
@groups[group_definition.name] << group_definition
|
41
48
|
end
|
42
|
-
# else
|
43
|
-
# tell project path to template class
|
44
|
-
Node::Template.project_path = @project.path
|
45
49
|
end
|
46
50
|
|
47
51
|
attr_reader :nodes, :groups, :project, :actions
|
@@ -64,7 +68,7 @@ class RBCM
|
|
64
68
|
end
|
65
69
|
log "parsing 'cap!'"
|
66
70
|
nodes.values.each do |node|
|
67
|
-
node.capabilities.each{|capability| node.sandbox.send "#{capability}!"}
|
71
|
+
node.capabilities.each{|capability| node.sandbox.send "#{capability.name}!"}
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
@@ -77,7 +81,9 @@ class RBCM
|
|
77
81
|
end
|
78
82
|
end
|
79
83
|
actions.checkable.each do |action|
|
80
|
-
|
84
|
+
actions.check.each do |check|
|
85
|
+
session.with(action.job.node.name.to_sym).exec check &block
|
86
|
+
end
|
81
87
|
end
|
82
88
|
session.loop
|
83
89
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rbcm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Wiegand
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - '='
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 4.2.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.11.3
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.11.3
|
55
69
|
description: manage your servers via simple config-files
|
56
70
|
email: martin@wiegand.tel
|
57
71
|
executables:
|
@@ -73,15 +87,18 @@ files:
|
|
73
87
|
- app/node/file.rb
|
74
88
|
- app/node/filesystem.rb
|
75
89
|
- app/node/job.rb
|
90
|
+
- app/node/job_search.rb
|
76
91
|
- app/node/node.rb
|
77
92
|
- app/node/remote.rb
|
78
93
|
- app/node/sandbox.rb
|
79
|
-
- app/
|
94
|
+
- app/project/addon.rb
|
80
95
|
- app/project/capability.rb
|
81
96
|
- app/project/definition.rb
|
82
97
|
- app/project/file.rb
|
83
98
|
- app/project/project.rb
|
84
99
|
- app/project/sandbox.rb
|
100
|
+
- app/project/template.rb
|
101
|
+
- app/project/template_list.rb
|
85
102
|
- app/rbcm.rb
|
86
103
|
- bin/rbcm
|
87
104
|
homepage: https://github.com/CroneKorkN/rbcm
|
data/app/node/template.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
class Node::Template
|
2
|
-
@@engines = [:erb, :mustache]
|
3
|
-
|
4
|
-
def initialize name:, capability: nil, context: {}
|
5
|
-
@name = name
|
6
|
-
@capability = capability
|
7
|
-
@context = context
|
8
|
-
end
|
9
|
-
|
10
|
-
def render
|
11
|
-
content = File.read path
|
12
|
-
layers.each do |layer|
|
13
|
-
if layer == :mustache
|
14
|
-
require "mustache"
|
15
|
-
content = Mustache.render(content, **@context)
|
16
|
-
elsif layer == :erb
|
17
|
-
# https://zaiste.net/rendering_erb_template_with_bindings_from_hash/
|
18
|
-
require "ostruct"; require "erb"
|
19
|
-
content = ERB.new(content).result(
|
20
|
-
OpenStruct.new(@context).instance_eval{binding}
|
21
|
-
)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
return content
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def path
|
30
|
-
@path ||= Dir["#{@@project_path}/**/#{@name}*"].first
|
31
|
-
end
|
32
|
-
|
33
|
-
def filename
|
34
|
-
File.basename(path)
|
35
|
-
end
|
36
|
-
|
37
|
-
def layers
|
38
|
-
@layers = []
|
39
|
-
filename.split(".").reverse.each.to_sym.each do |layer|
|
40
|
-
break unless @@engines.include? layer
|
41
|
-
@layers << layer
|
42
|
-
end
|
43
|
-
return @layers
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.project_path= path
|
47
|
-
@@project_path = path
|
48
|
-
end
|
49
|
-
end
|