hookit 0.7.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +3 -0
  6. data/Rakefile +10 -0
  7. data/bin/hookit +41 -0
  8. data/hookit.gemspec +30 -0
  9. data/lib/hookit/converginator.rb +141 -0
  10. data/lib/hookit/db.rb +37 -0
  11. data/lib/hookit/error.rb +8 -0
  12. data/lib/hookit/exit.rb +23 -0
  13. data/lib/hookit/helper/cron.rb +42 -0
  14. data/lib/hookit/helper/nfs.rb +113 -0
  15. data/lib/hookit/helper/shell.rb +35 -0
  16. data/lib/hookit/helper/xml.rb +25 -0
  17. data/lib/hookit/helper.rb +9 -0
  18. data/lib/hookit/hook.rb +102 -0
  19. data/lib/hookit/logger.rb +58 -0
  20. data/lib/hookit/logvac.rb +34 -0
  21. data/lib/hookit/platform/base.rb +11 -0
  22. data/lib/hookit/platform/smartos.rb +19 -0
  23. data/lib/hookit/platform/ubuntu.rb +19 -0
  24. data/lib/hookit/platform.rb +9 -0
  25. data/lib/hookit/platforms.rb +2 -0
  26. data/lib/hookit/registry.rb +53 -0
  27. data/lib/hookit/resource/base.rb +105 -0
  28. data/lib/hookit/resource/cron.rb +26 -0
  29. data/lib/hookit/resource/directory.rb +67 -0
  30. data/lib/hookit/resource/execute.rb +148 -0
  31. data/lib/hookit/resource/file.rb +71 -0
  32. data/lib/hookit/resource/hook_file.rb +80 -0
  33. data/lib/hookit/resource/link.rb +61 -0
  34. data/lib/hookit/resource/logrotate.rb +38 -0
  35. data/lib/hookit/resource/mount.rb +77 -0
  36. data/lib/hookit/resource/package.rb +74 -0
  37. data/lib/hookit/resource/rsync.rb +67 -0
  38. data/lib/hookit/resource/scp.rb +87 -0
  39. data/lib/hookit/resource/service.rb +89 -0
  40. data/lib/hookit/resource/socket.rb +67 -0
  41. data/lib/hookit/resource/template.rb +87 -0
  42. data/lib/hookit/resource/warning.rb +87 -0
  43. data/lib/hookit/resource/zfs.rb +99 -0
  44. data/lib/hookit/resource.rb +21 -0
  45. data/lib/hookit/resources.rb +15 -0
  46. data/lib/hookit/version.rb +3 -0
  47. data/lib/hookit.rb +27 -0
  48. metadata +205 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 37abbf6a175d07a169c2671f0bb48c160206d177
4
+ data.tar.gz: a1bb68175a4cc828109a2ce6c0240b9ef7baef67
5
+ SHA512:
6
+ metadata.gz: 2c3bcf3b055ebc427aa0c1110967ed85114ddb935bbe6ec64409e03eb1ad11605e25c4141c2c3c765b16cc6028c4898503d38b94a5fdb8f8eab4077bad5fb908
7
+ data.tar.gz: 75f85243cf7e675be73f22cff3f481a96433d06c9aab4a0762ad317efec47e1322e5bbd2c10b05f51b0e5fc5b596b688914e07420e0883df149c2224eb04cbc4
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hookit.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Pagoda Box
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Hookit
2
+
3
+ Hookit is the framework to provide hookit scripts with re-usable components and resources via an elegant dsl
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc "Create tag v#{Hookit::VERSION}"
4
+ task :tag do
5
+
6
+ puts "tagging version v#{Hookit::VERSION}"
7
+ `git tag -a v#{Hookit::VERSION} -m "Version #{Hookit::VERSION}"`
8
+ `git push origin --tags`
9
+
10
+ end
data/bin/hookit ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ MODULE_DIR = ENV['MODULE_DIR'] || "/opt/local/hookit/mod"
4
+ LOG_LEVEL = ENV['LOG_LEVEL'] || :error
5
+ LOGFILE = ENV['LOGFILE'] || '/var/log/hookit/hookit.log'
6
+
7
+ hook = ARGV.shift
8
+
9
+ if not hook
10
+ $stderr.puts "hook is required"
11
+ exit 1
12
+ end
13
+
14
+ # uncomment if dev only
15
+ lib = File.expand_path('../../lib', __FILE__)
16
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
17
+
18
+ require 'hookit'
19
+ require 'json'
20
+
21
+ include Hookit::Hook # payload helpers / resource dsl
22
+
23
+ set :log_level, LOG_LEVEL
24
+ set :logfile, LOGFILE
25
+ set :module_root, MODULE_DIR
26
+
27
+ # require hook libs
28
+ Dir.glob("#{MODULE_DIR}/lib/*.rb").each do |file|
29
+ require file
30
+ end
31
+
32
+ logger.info ""
33
+ logger.info "hook: #{hook}"
34
+ logger.info "payload: #{payload.to_json}"
35
+
36
+ begin
37
+ load "#{MODULE_DIR}/hooks/#{hook}.rb"
38
+ rescue LoadError
39
+ logger.error "hook: #{hook} does not exist"
40
+ $stderr.puts "hook: #{hook} does not exist"
41
+ end
data/hookit.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hookit/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hookit"
8
+ spec.version = Hookit::VERSION
9
+ spec.authors = ["Tyler Flint", "Greg Linton"]
10
+ spec.email = ["tyler@pagodabox.com"]
11
+ spec.summary = %q{Hookit is a framework to provide hookit scripts with re-usable components and resources via an elegant dsl.}
12
+ spec.description = %q{The core framework to provide hookit scripts with re-usable components.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'tilt'
22
+ spec.add_dependency 'erubis'
23
+ spec.add_dependency 'oj'
24
+ spec.add_dependency 'multi_json', '>= 1.3'
25
+ spec.add_dependency 'excon'
26
+ spec.add_dependency 'faraday'
27
+
28
+ spec.add_development_dependency "bundler"
29
+ spec.add_development_dependency "rake"
30
+ end
@@ -0,0 +1,141 @@
1
+ module Hookit
2
+ class Converginator
3
+
4
+ def initialize(map, list)
5
+ @map = map
6
+ @list = list
7
+ end
8
+
9
+ def converge!
10
+ output = {}
11
+ @map.each do |key, template|
12
+ if @list.key? key
13
+ output[key] = converge_value template, @list[key]
14
+ else
15
+ output[key] = template[:default]
16
+ end
17
+ end
18
+ output
19
+ end
20
+
21
+ def converge_value(template, value)
22
+ if template[:type] == :array
23
+ value = sanitize_array(template, value)
24
+ end
25
+
26
+ if valid? template, value
27
+ value
28
+ else
29
+ template[:default]
30
+ end
31
+ end
32
+
33
+ def sanitize_array(template, value)
34
+
35
+ case template[:of]
36
+ when :byte
37
+ value = [value] if ( valid_byte? value )
38
+ when :file
39
+ value = [value] if ( valid_file? value )
40
+ when :folder
41
+ value = [value] if ( valid_folder? value )
42
+ when :integer
43
+ value = [value] if ( valid_integer? value )
44
+ when :on_off
45
+ value = [value] if ( valid_on_off? value )
46
+ when :string
47
+ value = [value] if ( valid_string? value )
48
+ end
49
+ value
50
+ end
51
+
52
+ def valid?(template, value)
53
+ valid_type?(template, value) and valid_value?(template, value)
54
+ end
55
+
56
+ def valid_type?(template, value)
57
+ case template[:type]
58
+ when :array
59
+ valid_array? template, value
60
+ when :byte
61
+ valid_byte? value
62
+ when :file
63
+ valid_file? value
64
+ when :folder
65
+ valid_folder? value
66
+ when :hash
67
+ valid_hash? value
68
+ when :integer
69
+ valid_integer? value
70
+ when :on_off
71
+ valid_on_off? value
72
+ when :string
73
+ valid_string? value
74
+ end
75
+ end
76
+
77
+ def valid_value?(template, value)
78
+
79
+ return true if not template.key? :from
80
+
81
+ if template[:type] == :array
82
+ !( value.map {|element| template[:from].include? element} ).include? false
83
+ else
84
+ template[:from].include? value
85
+ end
86
+ end
87
+
88
+ def valid_string?(element)
89
+ element.is_a? String
90
+ end
91
+
92
+ def valid_array?(template, value)
93
+
94
+ return false if not value.is_a? Array
95
+
96
+ return true if not template.key? :of
97
+
98
+ case template[:of]
99
+ when :byte
100
+ !( value.map {|element| valid_byte? element} ).include? false
101
+ when :file
102
+ !( value.map {|element| valid_file? element} ).include? false
103
+ when :folder
104
+ !( value.map {|element| valid_folder? element} ).include? false
105
+ when :integer
106
+ !( value.map {|element| valid_integer? element} ).include? false
107
+ when :on_off
108
+ !( value.map {|element| valid_on_off? element} ).include? false
109
+ when :string
110
+ !( value.map {|element| valid_string? element} ).include? false
111
+ else
112
+ true
113
+ end
114
+ end
115
+
116
+ def valid_hash?(value)
117
+ value.is_a? Hash
118
+ end
119
+
120
+ def valid_integer?(value)
121
+ value.is_a? Integer || (value.to_i.to_s == value.to_s)
122
+ end
123
+
124
+ def valid_file?(value)
125
+ value =~ /^\/?[^\/]+(\/[^\/]+)*$/
126
+ end
127
+
128
+ def valid_folder?(value)
129
+ value =~ /^\/?[^\/]+(\/[^\/]+)*\/?$/
130
+ end
131
+
132
+ def valid_on_off?(value)
133
+ ['true', 'false', 'On', 'on', 'Off', 'off', '1', '0'].include? value.to_s
134
+ end
135
+
136
+ def valid_byte?(value)
137
+ value.to_s =~ /^\d+[BbKkMmGgTt]?$/
138
+ end
139
+
140
+ end
141
+ end
data/lib/hookit/db.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'oj'
2
+ require 'multi_json'
3
+ require 'fileutils'
4
+
5
+ module Hookit
6
+ class DB
7
+
8
+ DEFAULT_PATH = '/var/db/hookit/db.json'
9
+
10
+ def initialize(path=nil)
11
+ @path = path || DEFAULT_PATH
12
+ end
13
+
14
+ def fetch(key)
15
+ data[key]
16
+ end
17
+
18
+ def put(key, value)
19
+ data[key] = value
20
+ save
21
+ end
22
+
23
+ def load
24
+ ::MultiJson.load(::File.read(@path)) rescue {}
25
+ end
26
+
27
+ def save
28
+ ::FileUtils.mkdir_p(File.dirname(@path))
29
+ ::File.write(@path, ::MultiJson.dump(data))
30
+ end
31
+
32
+ def data
33
+ @data ||= load
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ module Hookit
2
+ module Error
3
+ class UnexpectedExit < StandardError; end
4
+ class UnknownAction < StandardError; end
5
+ class UnsupportedPlatform < StandardError; end
6
+ class UnsupportedOption < StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ module Hookit
2
+ module Exit
3
+ SUCCESS = 0
4
+ ERROR = 1
5
+ ERROR_RETRY = 2
6
+ ERROR_ENOMEM = 3
7
+ WAIT_2 = 11
8
+ WAIT_4 = 12
9
+ WAIT_8 = 13
10
+ WAIT_16 = 14
11
+ WAIT_32 = 15
12
+ WAIT_64 = 16
13
+ WAIT_128 = 17
14
+ WAIT_256 = 18
15
+ WAIT_512 = 19
16
+ WAIT_1024 = 20
17
+ ABORT = 21
18
+ ABORT_MSG = 22
19
+ SUCCESS_FLAG = 31
20
+ SUCCESS_MSG = 32
21
+ WTF = 255
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ module Hookit
2
+ module Helper
3
+ module Cron
4
+
5
+ MINUTES = 59
6
+ HOURS = 23
7
+ DAYS = 31
8
+ MONTHS = 12
9
+ WEEKDAY = 7
10
+
11
+ def sanitize_cron(cron)
12
+
13
+ time = cron.split(' ')
14
+
15
+ time[0] = compatible_cron(time[0],MINUTES)
16
+ time[1] = compatible_cron(time[1],HOURS)
17
+ time[2] = compatible_cron(time[2],DAYS, 1)
18
+ time[3] = compatible_cron(time[3],MONTHS, 1)
19
+ time[4] = compatible_cron(time[4],WEEKDAY)
20
+
21
+ time.join(' ')
22
+ end
23
+
24
+ protected
25
+
26
+ # converts */x cron format into solaris compatible format
27
+ def compatible_cron(time, limit, start = 0)
28
+ if time =~ /\//
29
+ increment = time.split('/')[1].to_i
30
+ x, y = start, []
31
+ for i in 0..limit/increment
32
+ y[i] = x
33
+ x +=increment
34
+ end
35
+ time = y.join(',')
36
+ end
37
+ time
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,113 @@
1
+ module Hookit
2
+ module Helper
3
+ module NFS
4
+
5
+ def sanitize_network_dirs(payload)
6
+ net_dirs = net_dirs(payload)
7
+
8
+ net_dirs.each do |component, dirs|
9
+ net_dirs[component] = clean_writables(dirs)
10
+ end
11
+
12
+ net_dirs
13
+ end
14
+
15
+ def net_dirs(payload)
16
+ key = payload[:storage].keys.first
17
+ boxfile = payload[:boxfile]
18
+
19
+ if boxfile[:shared_writable_dirs]
20
+ {
21
+ key => boxfile[:shared_writable_dirs]
22
+ }
23
+ elsif boxfile[:network_dirs].is_a? String
24
+ {
25
+ key => [boxfile[:network_dirs]]
26
+ }
27
+ elsif boxfile[:network_dirs].is_a? Array
28
+ {
29
+ key => boxfile[:network_dirs]
30
+ }
31
+ else
32
+ boxfile[:network_dirs] ||= {}
33
+ end
34
+
35
+ end
36
+
37
+ def clean_writables(dirs)
38
+ begin
39
+ dirs = dirs.map{|i| i.to_s}
40
+ rescue
41
+ dirs = [dirs].map{|i| i.to_s}
42
+ end
43
+ dirs = remove_empty(dirs)
44
+ dirs = filter_offensive(dirs)
45
+ dirs = strip_leading_slash(dirs)
46
+ dirs = strip_trailing_slash(dirs)
47
+ dirs = remove_nested(dirs)
48
+ dirs.uniq
49
+ end
50
+
51
+ def remove_empty(dirs)
52
+ dirs.inject([]) do |res, elem|
53
+ res << elem if elem && elem != ""
54
+ res
55
+ end
56
+ end
57
+
58
+ def filter_offensive(dirs)
59
+ dirs.inject([]) do |res, elem|
60
+ if elem[0] != '.'
61
+ # ensure not going up a directory
62
+ # ensure spaces are intended
63
+ # ensure directory is not . or /
64
+ unless elem =~ /(\*|\.?\.\/|(?<!\\)\s)|^\.$|^\/$/
65
+ res << elem
66
+ end
67
+ end
68
+ res
69
+ end
70
+ end
71
+
72
+ def strip_leading_slash(dirs)
73
+ dirs.inject([]) do |res, elem|
74
+ if elem[0] == '/'
75
+ elem.slice!(0)
76
+ end
77
+ res << elem
78
+ end
79
+ end
80
+
81
+ def strip_trailing_slash(dirs)
82
+ dirs.inject([]) do |res, elem|
83
+ if elem[-1] == '/'
84
+ elem.slice!(-1)
85
+ end
86
+ res << elem
87
+ end
88
+ end
89
+
90
+ # this removes nested mounts like:
91
+ # tmp/
92
+ # tmp/cache/
93
+ # tmp/assets/
94
+ #
95
+ # and keeps tmp/
96
+ def remove_nested(dirs)
97
+ dirs.sort!
98
+ dirs.inject([]) do |res, elem|
99
+ overlap = false
100
+ # now make sure parents dont contain children
101
+ res.each do |parent|
102
+ if elem =~ /^#{parent}\//
103
+ overlap = true
104
+ end
105
+ end
106
+ res << elem if not overlap
107
+ res
108
+ end
109
+ end
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,35 @@
1
+ module Hookit
2
+ module Helper
3
+ module Shell
4
+
5
+ def sanitize_shell_vars(vars)
6
+ vars.inject({}) do |res, (key,value)|
7
+ res[escape_shell_string(key.to_s)] = escape_shell_string(value.to_s)
8
+ res
9
+ end
10
+ end
11
+
12
+ # strategy:
13
+ # 1- escape the escapes
14
+ # 2- escape quotes
15
+ # 3- escape backticks
16
+ # 4- escape semicolons
17
+ # 5- escape ampersands
18
+ # 6- escape pipes
19
+ # 7- escape dollar signs
20
+ # 8- escape spaces
21
+ def escape_shell_string(str)
22
+ str = str.gsub(/\\/, "\\\\\\")
23
+ str = str.gsub(/"/, "\\\"")
24
+ str = str.gsub(/`/, "\\`")
25
+ str = str.gsub(/;/, "\\;")
26
+ str = str.gsub(/&/, "\\&")
27
+ str = str.gsub(/\|/, "\\|")
28
+ str = str.gsub(/\$/, "\\$")
29
+ str = str.gsub(/ /, "\\ ")
30
+ str
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ module Hookit
2
+ module Helper
3
+ module XML
4
+
5
+ def sanitize_xml_vars(vars)
6
+ vars.inject({}) do |res, (key,value)|
7
+ res[sanitize_xml_string(key.to_s)] = sanitize_xml_string(value.to_s)
8
+ res
9
+ end
10
+ end
11
+
12
+ protected
13
+
14
+ def sanitize_xml_string(str)
15
+ str = str.gsub(/&/, '&amp;')
16
+ str = str.gsub(/</, '&lt;')
17
+ str = str.gsub(/>/, '&gt;')
18
+ str = str.gsub(/"/, '&quot;')
19
+ str = str.gsub(/'/, '&apos;')
20
+ str
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ require 'hookit/helper/nfs'
2
+ require 'hookit/helper/shell'
3
+ require 'hookit/helper/xml'
4
+ require 'hookit/helper/cron'
5
+
6
+ module Hookit
7
+ module Helper
8
+ end
9
+ end
@@ -0,0 +1,102 @@
1
+ require 'oj'
2
+ require 'multi_json'
3
+
4
+ module Hookit
5
+ module Hook
6
+
7
+ def payload
8
+ @payload ||= parse_payload
9
+ end
10
+
11
+ def parse_payload
12
+ if not ARGV.empty?
13
+ MultiJson.load ARGV.first, symbolize_keys: true
14
+ else
15
+ {}
16
+ end
17
+ end
18
+
19
+ def converge(map, list)
20
+ Converginator.new(map, list).converge!
21
+ end
22
+
23
+ def registry(key, value=nil)
24
+ unless value.nil?
25
+ db.put(key, value)
26
+ else
27
+ db.fetch(key)
28
+ end
29
+ end
30
+
31
+ def db
32
+ @db ||= Hookit::DB.new
33
+ end
34
+
35
+ def dict
36
+ @dict ||= {}
37
+ end
38
+
39
+ def set(key, value)
40
+ dict[key] = value
41
+ end
42
+
43
+ def get(key)
44
+ dict[key]
45
+ end
46
+
47
+ def log(level, message)
48
+ logger.log level, message
49
+ end
50
+
51
+ def logger
52
+ @logger ||= Hookit::Logger.new(get(:logfile), get(:log_level))
53
+ end
54
+
55
+ def logvac
56
+ @logvac ||= Hookit::Logvac.new({
57
+ app: payload[:app][:id],
58
+ token: payload[:app][:logvac_token],
59
+ deploy: payload[:deploy][:id]
60
+ })
61
+ end
62
+
63
+ def platform
64
+ @platform ||= detect_platform
65
+ end
66
+
67
+ def detect_platform
68
+ Hookit.platforms.each do |key, value|
69
+ platform = value.new
70
+ if platform.detect?
71
+ return platform
72
+ end
73
+ end
74
+ false
75
+ end
76
+
77
+ # awesome resource-backed dsl
78
+ def method_missing(method_symbol, *args, &block)
79
+ resource_klass = Hookit.resources.get(method_symbol)
80
+ if resource_klass
81
+ resource = resource_klass.new(*args)
82
+ resource.dict = dict
83
+ resource.instance_eval(&block) if block_given?
84
+ if resource.can_run?
85
+ actions = resource.action
86
+ if actions.length > 1
87
+ res = {}
88
+ actions.each do |action|
89
+ res[action] = resource.run action
90
+ end
91
+ res
92
+ else
93
+ resource.run actions.first
94
+ end
95
+ end
96
+ else
97
+ super
98
+ end
99
+ end
100
+
101
+ end
102
+ end