factor 0.5.15 → 0.5.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/commands/base.rb +123 -0
- data/lib/commands/registry.rb +186 -0
- data/lib/commands/workflows.rb +112 -0
- data/lib/factor.rb +54 -0
- data/lib/factor/version.rb +6 -0
- data/lib/listener.rb +26 -0
- data/lib/runtime.rb +232 -0
- data/lib/websocket_manager.rb +94 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3df70093d41d893436a8b43a6d08f4ffe87249fb
|
4
|
+
data.tar.gz: 8b163aa8f9b1ebe4c9ddaf8c24f67d28917c2e10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e1d4fa2b2775bc2d34bc43acc8bed86f518d0b1865a56647679536454e1e0c52f0ee20db18d5cae137b256e4fafd8cf8f85b8c4c282eef88117482638c519cb
|
7
|
+
data.tar.gz: 29e9b70bf5ccf70e7cf1292c7ec4e8f11377c4a6efb1a96dc84c8b4939372a53151d486d73f9ee5464ee60b9a503f290b54ea56500101a240143f036d7ee6f87
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'colored'
|
4
|
+
require 'configatron'
|
5
|
+
require 'yaml'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module Factor
|
9
|
+
module Commands
|
10
|
+
# Base command with common methods used by all commands
|
11
|
+
class Command
|
12
|
+
DEFAULT_FILENAME = {
|
13
|
+
connectors: File.expand_path('./connectors.yml'),
|
14
|
+
credentials: File.expand_path('./credentials.yml')
|
15
|
+
}
|
16
|
+
|
17
|
+
attr_accessor :destination_stream
|
18
|
+
|
19
|
+
def info(options = {})
|
20
|
+
log_line :info, options
|
21
|
+
end
|
22
|
+
|
23
|
+
def error(options = {})
|
24
|
+
log_line :error, options
|
25
|
+
end
|
26
|
+
|
27
|
+
def warn(options = {})
|
28
|
+
log_line :warn, options
|
29
|
+
end
|
30
|
+
|
31
|
+
def success(options = {})
|
32
|
+
log_line :success, options
|
33
|
+
end
|
34
|
+
|
35
|
+
def exception(message, exception)
|
36
|
+
error 'message' => message
|
37
|
+
error 'message' => " #{exception.message}"
|
38
|
+
exception.backtrace.each do |line|
|
39
|
+
error 'message' => " #{line}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_config(options = {})
|
44
|
+
load_config_data :credentials, options
|
45
|
+
load_config_data :connectors, options
|
46
|
+
end
|
47
|
+
|
48
|
+
def save_config(options={})
|
49
|
+
credentials_relative_path = options[:credentials] || DEFAULT_FILENAME[:credentials]
|
50
|
+
credentials_absolute_path = File.expand_path(credentials_relative_path)
|
51
|
+
connectors_relative_path = options[:connectors] || DEFAULT_FILENAME[:connectors]
|
52
|
+
connectors_absolute_path = File.expand_path(connectors_relative_path)
|
53
|
+
|
54
|
+
connectors = Hash[stringify(configatron.connectors.to_h).sort]
|
55
|
+
credentials = Hash[stringify(configatron.credentials.to_h).sort]
|
56
|
+
|
57
|
+
File.write(connectors_absolute_path,YAML.dump(connectors))
|
58
|
+
File.write(credentials_absolute_path,YAML.dump(credentials))
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def stringify(hash)
|
64
|
+
hash.inject({}) do |options, (key, value)|
|
65
|
+
options[key.to_s] = value.is_a?(Hash) ? stringify(value) : value
|
66
|
+
options
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_config_data(config_type, options = {})
|
71
|
+
relative_path = options[config_type] || DEFAULT_FILENAME[config_type]
|
72
|
+
absolute_path = File.expand_path(relative_path)
|
73
|
+
begin
|
74
|
+
data = YAML.load(File.read(absolute_path))
|
75
|
+
rescue
|
76
|
+
data = {}
|
77
|
+
end
|
78
|
+
configatron[config_type].configure_from_hash(data)
|
79
|
+
rescue => ex
|
80
|
+
exception "Couldn't load #{config_type} from #{absolute_path}", ex
|
81
|
+
end
|
82
|
+
|
83
|
+
def log_line(section, options = {})
|
84
|
+
options = { message: options } if options.is_a?(String)
|
85
|
+
tag = tag(options)
|
86
|
+
message = options['message'] || options[:message]
|
87
|
+
section_text = format_section(section)
|
88
|
+
write "[ #{section_text} ] [#{time}]#{tag} #{message}" if message
|
89
|
+
end
|
90
|
+
|
91
|
+
def format_section(section)
|
92
|
+
formated_section = section.to_s.upcase.center(10)
|
93
|
+
case section
|
94
|
+
when :error then formated_section.red
|
95
|
+
when :info then formated_section.bold
|
96
|
+
when :warn then formated_section.yellow
|
97
|
+
when :success then formated_section.green
|
98
|
+
else formated_section
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def tag(options)
|
103
|
+
primary = options['service_id'] || options['instance_id']
|
104
|
+
secondary = if options['service_id'] && options['instance_id']
|
105
|
+
":#{options['instane_id']}"
|
106
|
+
else
|
107
|
+
''
|
108
|
+
end
|
109
|
+
primary ? "[#{primary}#{secondary}]" : ''
|
110
|
+
end
|
111
|
+
|
112
|
+
def time
|
113
|
+
Time.now.localtime.strftime('%m/%d/%y %T.%L')
|
114
|
+
end
|
115
|
+
|
116
|
+
def write(message)
|
117
|
+
stream = @destination_stream || $stdout
|
118
|
+
stream.puts(message)
|
119
|
+
stream.flush
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'rest-client'
|
5
|
+
require 'erubis'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
require 'commands/base'
|
9
|
+
|
10
|
+
module Factor
|
11
|
+
module Commands
|
12
|
+
# Workflow is a Command to start the factor runtime from the CLI
|
13
|
+
class Registry < Factor::Commands::Command
|
14
|
+
|
15
|
+
def workflows(args, options)
|
16
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/workflows.yml'
|
17
|
+
|
18
|
+
list.each do |id, values|
|
19
|
+
puts "#{values['name'].bold} (#{id}): #{values['description']}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def connectors(args, options)
|
24
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/connectors.yml'
|
25
|
+
|
26
|
+
list.each do |id, values|
|
27
|
+
puts "#{values['name'].bold} (#{id})"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_connector(args, options)
|
32
|
+
puts "Workflow ID is required (factor registry connector add --help)".red unless args[0]
|
33
|
+
|
34
|
+
setup_connector args[0].to_s, options if args[0]
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_workflow(args, options)
|
38
|
+
begin
|
39
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/workflows.yml'
|
40
|
+
rescue
|
41
|
+
puts "Couldn't connect to the server to get connector information".red
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
unless args[0]
|
46
|
+
puts "Workflow ID is required".red
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
begin
|
51
|
+
workflow_id = args[0].to_s
|
52
|
+
workflow_info = list[workflow_id]
|
53
|
+
config_url = workflow_info['config']
|
54
|
+
workflow_name = workflow_info['name']
|
55
|
+
rescue
|
56
|
+
puts "Couldn't find information for #{workflow_id}".red
|
57
|
+
exit
|
58
|
+
end
|
59
|
+
|
60
|
+
load_config(credentials:options.credentials, connectors:options.connectors)
|
61
|
+
|
62
|
+
begin
|
63
|
+
config = get_json_data(config_url)
|
64
|
+
rescue
|
65
|
+
puts "Couldn't pull up configuration information from #{config_url}".red
|
66
|
+
exit
|
67
|
+
end
|
68
|
+
|
69
|
+
if !config['required-connectors'] || !config['required-connectors'].is_a?(Array) || !config['variables'] || !config['variables'].is_a?(Hash)
|
70
|
+
puts "Configuration information for the workflow is missing information"
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
|
74
|
+
config['required-connectors'].each do |connector_id|
|
75
|
+
if configatron.credentials.to_hash[connector_id.to_sym]
|
76
|
+
puts "#{connector_id} already configured".green
|
77
|
+
else
|
78
|
+
setup_connector(connector_id,options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
variables = {}
|
83
|
+
config['variables'].each do |var_id,var_info|
|
84
|
+
puts var_info['description'] if var_info['description']
|
85
|
+
values = begin
|
86
|
+
JSON.parse(options.values)
|
87
|
+
rescue
|
88
|
+
{}
|
89
|
+
end
|
90
|
+
variables[var_id] = values[var_id]
|
91
|
+
variables[var_id] ||= ask("#{var_info['name'].bold}#{' (optional)' if var_info['optional']}: ").to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
begin
|
95
|
+
template = RestClient.get(config['template'])
|
96
|
+
rescue
|
97
|
+
puts "Couldn't find a template at #{config['template']}".red
|
98
|
+
exit
|
99
|
+
end
|
100
|
+
|
101
|
+
begin
|
102
|
+
eruby = Erubis::Eruby.new(template)
|
103
|
+
workflow_content = eruby.result(variables)
|
104
|
+
rescue
|
105
|
+
puts "Failed to generate template".red
|
106
|
+
exit
|
107
|
+
end
|
108
|
+
|
109
|
+
workflow_filename = "workflow-#{workflow_id}.rb"
|
110
|
+
begin
|
111
|
+
File.write(workflow_filename, workflow_content)
|
112
|
+
rescue
|
113
|
+
puts "Failed to write the file to disk. Check permissions.".red
|
114
|
+
exit
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
puts "Created #{workflow_name} successfully".green
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def setup_connector(connector_id, options)
|
124
|
+
begin
|
125
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/connectors.yml'
|
126
|
+
rescue
|
127
|
+
puts "Couldn't connect to the server to get connector information".red
|
128
|
+
exit
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
begin
|
133
|
+
connector_info = list[connector_id]
|
134
|
+
connector_name = connector_info['name']
|
135
|
+
new_connectors = connector_info['connectors']
|
136
|
+
required_credentials = connector_info['credentials']
|
137
|
+
rescue
|
138
|
+
puts "Couldn't find information for '#{connector_id}'".red
|
139
|
+
exit
|
140
|
+
end
|
141
|
+
|
142
|
+
unless connector_name && new_connectors && required_credentials
|
143
|
+
puts "Couldn't find information for '#{connector_id}'".red
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
load_config(credentials:options.credentials, connectors:options.connectors)
|
148
|
+
connectors = configatron.connectors.to_hash
|
149
|
+
credentials = configatron.credentials.to_hash
|
150
|
+
|
151
|
+
connectors[connector_id] = new_connectors
|
152
|
+
|
153
|
+
required_credentials.each do |credential_id, credential_info|
|
154
|
+
puts credential_info['description'] if credential_info['description']
|
155
|
+
credentials[connector_id] ||= {}
|
156
|
+
values = begin
|
157
|
+
JSON.parse(options.values)
|
158
|
+
rescue
|
159
|
+
{}
|
160
|
+
end
|
161
|
+
credentials[connector_id][credential_id.to_s] = values[credential_id.to_s]
|
162
|
+
credentials[connector_id][credential_id.to_s] ||= ask("#{connector_name.bold} #{credential_info['name'].bold}#{' (optional)' if credential_info['optional']}: ").to_s
|
163
|
+
end
|
164
|
+
|
165
|
+
configatron[:credentials].configure_from_hash(credentials)
|
166
|
+
configatron[:connectors].configure_from_hash(connectors)
|
167
|
+
|
168
|
+
save_config(credentials:options.credentials, connectors:options.connectors)
|
169
|
+
|
170
|
+
puts "Setup #{connector_name} successfully".green
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_yaml_data(url)
|
174
|
+
raw_content = RestClient.get(url)
|
175
|
+
list = YAML.parse(raw_content).to_ruby
|
176
|
+
list
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_json_data(url)
|
180
|
+
raw_content = RestClient.get(url)
|
181
|
+
data = JSON.parse(raw_content)
|
182
|
+
data
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'configatron'
|
4
|
+
|
5
|
+
require 'commands/base'
|
6
|
+
require 'runtime'
|
7
|
+
|
8
|
+
module Factor
|
9
|
+
module Commands
|
10
|
+
# Workflow is a Command to start the factor runtime from the CLI
|
11
|
+
class Workflow < Factor::Commands::Command
|
12
|
+
def initialize
|
13
|
+
@workflows = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def server(_args, options)
|
17
|
+
config_settings = {}
|
18
|
+
config_settings[:credentials] = options.credentials
|
19
|
+
config_settings[:connectors] = options.connectors
|
20
|
+
workflow_filename = File.expand_path(options.path || '.')
|
21
|
+
@destination_stream = File.new(options.log, 'w+') if options.log
|
22
|
+
|
23
|
+
load_config(config_settings)
|
24
|
+
load_all_workflows(workflow_filename)
|
25
|
+
block_until_interupt
|
26
|
+
log_message 'status' => 'info', 'message' => 'Good bye!'
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def load_all_workflows(workflow_filename)
|
32
|
+
glob_ending = workflow_filename[-1] == '/' ? '' : '/'
|
33
|
+
glob = "#{workflow_filename}#{glob_ending}*.rb"
|
34
|
+
file_list = Dir.glob(glob)
|
35
|
+
if !file_list.all? { |file| File.file?(file) }
|
36
|
+
error "#{workflow_filename} is neither a file or directory"
|
37
|
+
elsif file_list.count == 0
|
38
|
+
error 'No workflows in this directory to run'
|
39
|
+
else
|
40
|
+
file_list.each { |filename| load_workflow(filename) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def block_until_interupt
|
45
|
+
log_message 'status' => 'info', 'message' => 'Ctrl-c to exit'
|
46
|
+
begin
|
47
|
+
loop do
|
48
|
+
sleep 1
|
49
|
+
end
|
50
|
+
rescue Interrupt
|
51
|
+
log_message 'status' => 'info', 'message' => 'Exiting app...'
|
52
|
+
ensure
|
53
|
+
@workflows.keys.each { |filename| unload_workflow(filename) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_workflow(filename)
|
58
|
+
workflow_filename = File.expand_path(filename)
|
59
|
+
log_message(
|
60
|
+
'status' => 'info',
|
61
|
+
'message' => "Loading workflow from #{workflow_filename}")
|
62
|
+
begin
|
63
|
+
workflow_definition = File.read(workflow_filename)
|
64
|
+
rescue => ex
|
65
|
+
exception "Couldn't read file #{workflow_filename}", ex
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
log_message(
|
70
|
+
'status' => 'info',
|
71
|
+
'message' => 'Setting up workflow processor')
|
72
|
+
begin
|
73
|
+
connector_settings = configatron.connectors.to_hash
|
74
|
+
credential_settings = configatron.credentials.to_hash
|
75
|
+
runtime = Runtime.new(connector_settings, credential_settings)
|
76
|
+
runtime.logger = method(:log_message)
|
77
|
+
rescue => ex
|
78
|
+
message = "Couldn't setup workflow process for #{workflow_filename}"
|
79
|
+
exception message, ex
|
80
|
+
end
|
81
|
+
|
82
|
+
@workflows[workflow_filename] = fork do
|
83
|
+
begin
|
84
|
+
log_message(
|
85
|
+
'status' => 'info',
|
86
|
+
'message' => "Starting #{workflow_filename}")
|
87
|
+
runtime.load(workflow_definition)
|
88
|
+
rescue => ex
|
89
|
+
exception "Couldn't load #{workflow_filename}", ex
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def unload_workflow(filename)
|
95
|
+
workflow_filename = File.expand_path(filename)
|
96
|
+
log_message(
|
97
|
+
'status' => 'info',
|
98
|
+
'message' => "Stopping #{workflow_filename}")
|
99
|
+
Process.kill('SIGINT', @workflows[workflow_filename])
|
100
|
+
end
|
101
|
+
|
102
|
+
def log_message(message_info)
|
103
|
+
case message_info['status']
|
104
|
+
when 'info' then info message_info
|
105
|
+
when 'success' then success message_info
|
106
|
+
when 'warn' then warn message_info
|
107
|
+
else error message_info
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/factor.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'commander/import'
|
4
|
+
|
5
|
+
require 'factor/version'
|
6
|
+
require 'commands/workflows'
|
7
|
+
require 'commands/registry'
|
8
|
+
|
9
|
+
program :name, 'Factor.io Server'
|
10
|
+
program :version, Factor::VERSION
|
11
|
+
program :description, 'Factor.io Server to run workflows'
|
12
|
+
|
13
|
+
command 'server' do |c|
|
14
|
+
c.syntax = 'factor server [options]'
|
15
|
+
c.description = 'Start the Factor.io Server in the current local directory'
|
16
|
+
c.option '--log FILE', String, 'Log file path. Default is stdout.'
|
17
|
+
c.option '--credentials FILE', String, 'credentials.yml file path.'
|
18
|
+
c.option '--connectors FILE', String, 'connectors.yml file path'
|
19
|
+
c.option '--path FILE', String, 'Path to workflows'
|
20
|
+
c.when_called Factor::Commands::Workflow, :server
|
21
|
+
end
|
22
|
+
|
23
|
+
command 'registry workflows' do |c|
|
24
|
+
c.syntax = 'factor registry workflows'
|
25
|
+
c.description = 'Get list of available workflow jumpstarts'
|
26
|
+
c.when_called Factor::Commands::Registry, :workflows
|
27
|
+
end
|
28
|
+
|
29
|
+
command 'registry workflows add' do |c|
|
30
|
+
c.syntax = 'factor registry workflow add <id>'
|
31
|
+
c.description = 'Get list of available workflows'
|
32
|
+
c.option '--credentials FILE', String, 'credentials.yml file path.'
|
33
|
+
c.option '--connectors FILE', String, 'connectors.yml file path'
|
34
|
+
c.option '--values \'{"api_key":"foo"}\'', String, "{}"
|
35
|
+
c.when_called Factor::Commands::Registry, :add_workflow
|
36
|
+
end
|
37
|
+
|
38
|
+
command 'registry connectors' do |c|
|
39
|
+
c.syntax = 'factor registry connectors'
|
40
|
+
c.description = 'Get list of available connectors'
|
41
|
+
c.when_called Factor::Commands::Registry, :connectors
|
42
|
+
end
|
43
|
+
|
44
|
+
command 'registry connector add' do |c|
|
45
|
+
c.syntax = 'factor registry connector add <id>'
|
46
|
+
c.description = 'Get list of available connectors'
|
47
|
+
c.option '--credentials FILE', String, 'credentials.yml file path.'
|
48
|
+
c.option '--connectors FILE', String, 'connectors.yml file path'
|
49
|
+
c.option '--values \'{"api_key":"foo"}\'', String, "{}"
|
50
|
+
c.when_called Factor::Commands::Registry, :add_connector
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_command 's', 'server'
|
54
|
+
alias_command 'r', 'registry'
|
data/lib/listener.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'websocket_manager'
|
4
|
+
|
5
|
+
module Factor
|
6
|
+
# Class Listener for integrating with connector service
|
7
|
+
class Listener
|
8
|
+
def initialize(url)
|
9
|
+
@url = url
|
10
|
+
end
|
11
|
+
|
12
|
+
def listener(listener_id)
|
13
|
+
listen("#{@url}/listeners/#{listener_id}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def action(action_id)
|
17
|
+
listen("#{@url}/actions/#{action_id}")
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def listen(uri_path)
|
23
|
+
WebSocketManager.new(uri_path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/runtime.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'yaml'
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'uri'
|
8
|
+
require 'faye/websocket'
|
9
|
+
require 'ostruct'
|
10
|
+
|
11
|
+
require 'listener'
|
12
|
+
require 'commands/base'
|
13
|
+
|
14
|
+
module Factor
|
15
|
+
# Runtime class is the magic of the server
|
16
|
+
class Runtime
|
17
|
+
attr_accessor :logger, :name, :description, :id, :instance_id, :connectors, :credentials
|
18
|
+
|
19
|
+
def initialize(connectors, credentials)
|
20
|
+
@workflow_spec = {}
|
21
|
+
@sockets = []
|
22
|
+
@instance_id = SecureRandom.hex(3)
|
23
|
+
@reconnect = true
|
24
|
+
|
25
|
+
trap 'SIGINT' do
|
26
|
+
info "Exiting '#{@instance_id}'"
|
27
|
+
@reconnect = false
|
28
|
+
@sockets.each { |s| s.close }
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
@connectors = {}
|
33
|
+
flat_hash(connectors).each do |key, connector_url|
|
34
|
+
@connectors[key] = Listener.new(connector_url)
|
35
|
+
end
|
36
|
+
|
37
|
+
@credentials = {}
|
38
|
+
credentials.each do |connector_id, credential_settings|
|
39
|
+
@credentials[connector_id] = credential_settings
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def load(workflow_definition)
|
44
|
+
EM.run do
|
45
|
+
instance_eval(workflow_definition)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def listen(service_ref, params = {}, &block)
|
50
|
+
service_map = service_ref.split('::')
|
51
|
+
service_id = service_map.first
|
52
|
+
listener_id = service_map.last
|
53
|
+
service_key = service_map[0..-2].map{|k| k.to_sym}
|
54
|
+
|
55
|
+
ws = @connectors[service_key].listener(listener_id)
|
56
|
+
|
57
|
+
handle_on_open(service_ref, 'Listener', ws, params)
|
58
|
+
|
59
|
+
ws.on :close do
|
60
|
+
error 'Listener disconnected'
|
61
|
+
if @reconnect
|
62
|
+
warn 'Reconnecting...'
|
63
|
+
sleep 3
|
64
|
+
ws.open
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
ws.on :message do |event|
|
69
|
+
listener_response = JSON.parse(event.data)
|
70
|
+
case listener_response['type']
|
71
|
+
when'start_workflow'
|
72
|
+
success "Workflow '#{service_id}::#{listener_id}' triggered"
|
73
|
+
error_handle_call(listener_response, &block)
|
74
|
+
when 'return'
|
75
|
+
success "Workflow '#{service_ref}' started"
|
76
|
+
when 'fail'
|
77
|
+
error "Workflow '#{service_ref}' failed to start"
|
78
|
+
when 'log'
|
79
|
+
listener_response['message'] = " #{listener_response['message']}"
|
80
|
+
log_message(listener_response)
|
81
|
+
else
|
82
|
+
error "Unknown listener response: #{listener_response}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
ws.on :retry do |event|
|
87
|
+
warn event[:message]
|
88
|
+
end
|
89
|
+
|
90
|
+
ws.on :error do |event|
|
91
|
+
err = 'Error during WebSocket handshake: Unexpected response code: 401'
|
92
|
+
if event.message == err
|
93
|
+
error "Sorry but you don't have access to this listener,
|
94
|
+
| either because your token is invalid or your plan doesn't
|
95
|
+
| support this listener"
|
96
|
+
else
|
97
|
+
error 'Failure in WebSocket connection to connector service'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
ws.open
|
102
|
+
|
103
|
+
@sockets << ws
|
104
|
+
end
|
105
|
+
|
106
|
+
def run(service_ref, params = {}, &block)
|
107
|
+
service_map = service_ref.split('::')
|
108
|
+
service_id = service_map.first
|
109
|
+
action_id = service_map.last
|
110
|
+
service_key = service_map[0..-2].map{|k| k.to_sym}
|
111
|
+
|
112
|
+
ws = @connectors[service_key].action(action_id)
|
113
|
+
|
114
|
+
handle_on_open(service_ref, 'Action', ws, params)
|
115
|
+
|
116
|
+
ws.on :error do
|
117
|
+
error 'Connection dropped while calling action'
|
118
|
+
end
|
119
|
+
|
120
|
+
ws.on :message do |event|
|
121
|
+
action_response = JSON.parse(event.data)
|
122
|
+
case action_response['type']
|
123
|
+
when 'return'
|
124
|
+
ws.close
|
125
|
+
success "Action '#{service_ref}' responded"
|
126
|
+
error_handle_call(action_response, &block)
|
127
|
+
when 'fail'
|
128
|
+
ws.close
|
129
|
+
error " #{action_response['message']}"
|
130
|
+
error "Action '#{service_ref}' failed"
|
131
|
+
when 'log'
|
132
|
+
action_response['message'] = " #{action_response['message']}"
|
133
|
+
log_message(action_response)
|
134
|
+
else
|
135
|
+
error "Unknown action response: #{action_response}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
ws.open
|
140
|
+
|
141
|
+
@sockets << ws
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
class DeepStruct < OpenStruct
|
147
|
+
def initialize(hash=nil)
|
148
|
+
@table = {}
|
149
|
+
@hash_table = {}
|
150
|
+
|
151
|
+
if hash
|
152
|
+
hash.each do |k,v|
|
153
|
+
@table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
|
154
|
+
@hash_table[k.to_sym] = v
|
155
|
+
|
156
|
+
new_ostruct_member(k)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def to_h
|
162
|
+
@hash_table
|
163
|
+
end
|
164
|
+
|
165
|
+
def [](idx)
|
166
|
+
hash = marshal_dump
|
167
|
+
hash[idx.to_sym]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def simple_object_convert(item)
|
172
|
+
if item.is_a?(Hash)
|
173
|
+
DeepStruct.new(item)
|
174
|
+
elsif item.is_a?(Array)
|
175
|
+
item.map do |i|
|
176
|
+
simple_object_convert(i)
|
177
|
+
end
|
178
|
+
else
|
179
|
+
item
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def flat_hash(h,f=[],g={})
|
184
|
+
return g.update({ f=>h }) unless h.is_a? Hash
|
185
|
+
h.each { |k,r| flat_hash(r,f+[k],g) }
|
186
|
+
g
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_on_open(service_ref, dsl_type, ws, params)
|
190
|
+
service_map = service_ref.split('::')
|
191
|
+
service_id = service_map.first
|
192
|
+
|
193
|
+
ws.on :open do
|
194
|
+
params.merge!(@credentials[service_id.to_sym] || {})
|
195
|
+
success "#{dsl_type.capitalize} '#{service_ref}' called"
|
196
|
+
ws.send(params.to_json)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def error_handle_call(listener_response, &block)
|
201
|
+
content = simple_object_convert(listener_response['payload'])
|
202
|
+
block.call(content) if block
|
203
|
+
rescue => ex
|
204
|
+
error "Error in workflow definition: #{ex.message}"
|
205
|
+
ex.backtrace.each do |line|
|
206
|
+
error " #{line}"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def success(msg)
|
211
|
+
log_message('type' => 'log', 'status' => 'success', 'message' => msg)
|
212
|
+
end
|
213
|
+
|
214
|
+
def warn(msg)
|
215
|
+
log_message('type' => 'log', 'status' => 'warn', 'message' => msg)
|
216
|
+
end
|
217
|
+
|
218
|
+
def error(msg)
|
219
|
+
log_message('type' => 'log', 'status' => 'error', 'message' => msg)
|
220
|
+
end
|
221
|
+
|
222
|
+
def info(msg)
|
223
|
+
log_message('type' => 'log', 'status' => 'info', 'message' => msg)
|
224
|
+
end
|
225
|
+
|
226
|
+
def log_message(message_info)
|
227
|
+
message_info['instance_id'] = @instance_id
|
228
|
+
message_info['workflow_id'] = @id
|
229
|
+
@logger.call(message_info) if @logger
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'faye/websocket'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Factor
|
7
|
+
# class for managing the web socket connections
|
8
|
+
class WebSocketManager
|
9
|
+
attr_accessor :keep_open, :events, :state
|
10
|
+
|
11
|
+
def initialize(uri, headers = {})
|
12
|
+
u = URI(uri)
|
13
|
+
@uri = u.to_s
|
14
|
+
@settings = { ping: 10, retry: 5 }
|
15
|
+
@settings[:headers] = headers if headers && headers != {}
|
16
|
+
@state = :closed
|
17
|
+
@events = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def open
|
21
|
+
if closed?
|
22
|
+
@state = :opening
|
23
|
+
connect
|
24
|
+
end
|
25
|
+
@state
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
if open?
|
30
|
+
@state = :closing
|
31
|
+
@ws.close
|
32
|
+
end
|
33
|
+
@state
|
34
|
+
end
|
35
|
+
|
36
|
+
def on(event, &block)
|
37
|
+
@events[event] = block
|
38
|
+
end
|
39
|
+
|
40
|
+
def open?
|
41
|
+
@state == :open
|
42
|
+
end
|
43
|
+
|
44
|
+
def opening?
|
45
|
+
@state == :opening
|
46
|
+
end
|
47
|
+
|
48
|
+
def closed?
|
49
|
+
@state == :closed
|
50
|
+
end
|
51
|
+
|
52
|
+
def closing?
|
53
|
+
@state == :closing
|
54
|
+
end
|
55
|
+
|
56
|
+
def send(msg)
|
57
|
+
@ws.send(msg)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def call_event(event, data)
|
63
|
+
@events[event].call(data) if @events[event]
|
64
|
+
end
|
65
|
+
|
66
|
+
def connect
|
67
|
+
EM.run do
|
68
|
+
begin
|
69
|
+
@ws = Faye::WebSocket::Client.new(@uri, nil, @settings)
|
70
|
+
|
71
|
+
@ws.on :close do |event|
|
72
|
+
@state = :closed
|
73
|
+
call_event :close, event
|
74
|
+
end
|
75
|
+
|
76
|
+
@ws.on :message do |msg|
|
77
|
+
call_event :message, msg
|
78
|
+
end
|
79
|
+
|
80
|
+
@ws.on :open do |event|
|
81
|
+
@state = :open
|
82
|
+
call_event :open, event
|
83
|
+
end
|
84
|
+
|
85
|
+
@ws.on :error do |event|
|
86
|
+
call_event :error, event
|
87
|
+
end
|
88
|
+
rescue => ex
|
89
|
+
call_event :fail, ex.message
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maciej Skierkowski
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: commander
|
@@ -150,6 +150,14 @@ files:
|
|
150
150
|
- "./spec/spec_helper.rb"
|
151
151
|
- "./spec/workflow_spec.rb"
|
152
152
|
- bin/factor
|
153
|
+
- lib/commands/base.rb
|
154
|
+
- lib/commands/registry.rb
|
155
|
+
- lib/commands/workflows.rb
|
156
|
+
- lib/factor.rb
|
157
|
+
- lib/factor/version.rb
|
158
|
+
- lib/listener.rb
|
159
|
+
- lib/runtime.rb
|
160
|
+
- lib/websocket_manager.rb
|
153
161
|
homepage: https://factor.io
|
154
162
|
licenses: []
|
155
163
|
metadata: {}
|