fdk 0.0.13 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: cd1f63ae8fc93bda3870e3ce780994e84b99e386
4
- data.tar.gz: 045eebb796bd9eaae7a190dbd10c3bbb390c61a6
2
+ SHA256:
3
+ metadata.gz: a50737dfa816cbaa0d9de365617db0b1c393d47984529e6de1ac3fdf6e1f9167
4
+ data.tar.gz: 8e843ba19e95fc057ec571ec05dc6e4460761bfc5aa2f3bce81a4f068bfb31ed
5
5
  SHA512:
6
- metadata.gz: 31677d766e3ae7d1dff6ccca614e5be549e5704dd2747406206496f3c2ec886fa48735d3fde66989b4840606b692f6b4cc2bde95da5c717f7ab5dedd2b70da57
7
- data.tar.gz: 7a4d3a7d572e94f752ec2be0eacc44bcc49936643d7c9a63faf7ee57d4a832fa362a95c2daf095d5ad4718a06e469a853f1f2442bac21d6434157928983af2c3
6
+ metadata.gz: 67c53e7e572ca2a7f3c894ebe111b07fbf86df79e407ab10e2484e0479a0f67d02f45e504b46af9c5a2c81763994c6986e7f9a7d768ee8c341d175800b2bf013
7
+ data.tar.gz: ef3b16817052b5eee6ea90d7c0bbd8158446097a87df2db0d2dfad071beed80044c6bad14a8731064340909b3f9ae5ca6338498874864fae1d2709f6a3559f7e
data/README.md CHANGED
@@ -26,7 +26,7 @@ an FDK::Response object instead of a string.
26
26
  Then simply pass that function to the FDK:
27
27
 
28
28
  ```ruby
29
- FDK.handle(:myfunction)
29
+ FDK.handle(target: :myfunction)
30
30
  ```
31
31
 
32
32
  ## Examples
@@ -46,7 +46,7 @@ def myfunction(context:, input:)
46
46
  { message: "Hello #{name}!" }
47
47
  end
48
48
 
49
- FDK.handle(function: :myfunction)
49
+ FDK.handle(target: :myfunction)
50
50
  ```
51
51
 
52
52
  ## Deploying functions
@@ -85,15 +85,7 @@ $ fn invoke examples hello
85
85
  To get a more personal message, send a name in a JSON format and set the
86
86
  `content-type 'application/json'`:
87
87
  ```
88
- echo '{"name":"Joe"}' | fn invoke examples hello --content-type
89
- 'application/json'
88
+ echo '{"name":"Joe"}' | fn invoke examples hello --content-type 'application/json'
90
89
  {"message":"Hello Joe!"}
91
90
  ```
92
91
 
93
- ## Compare cold and hot functions
94
-
95
- Run [loop.rb](examples/loop.rb)
96
-
97
- ```sh
98
- ruby loop.rb
99
- ```
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ task default: %w[test]
2
+
3
+ task :test do
4
+ ruby "tests/test_fdk.rb"
5
+ end
data/lib/fdk/context.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'date'
2
+
1
3
  module FDK
2
4
 
3
5
  # Config looks up values in the env vars
@@ -7,50 +9,146 @@ module FDK
7
9
  end
8
10
  end
9
11
 
10
- class Context
11
12
 
12
- # TODO: Rethink FN_PATH, if it's a reference to the route, maybe it should be FN_ROUTE? eg: if it's a dynamic path, this env var would
13
- # show the route's path (ie: route identifier), eg: /users/:name, not the actual path.
13
+ class InHeaders
14
+ def initialize (h, key_fn)
15
+ @headers = h
16
+ @key_fn = key_fn
17
+
18
+ end
19
+
20
+ def headerKey(key)
21
+ if @key_fn
22
+ key = @key_fn.call(key)
23
+ end
24
+ key.downcase
25
+ end
26
+
27
+ def [](key)
28
+ h = @headers[headerKey(key)]
29
+ unless h.nil?
30
+ return h[0]
31
+ end
32
+ nil
33
+ end
34
+
35
+ def each (&block)
36
+ @headers.each &block
37
+ end
38
+ end
39
+
40
+ class OutHeaders < InHeaders
41
+
42
+ def initialize(h, key_in_fn)
43
+ super(h, key_in_fn)
44
+ end
45
+
14
46
 
47
+ def []=(key, value)
48
+ if value.is_a? Array
49
+ h = []
50
+ value.each {|x| h.push(x.to_s)}
51
+ @headers[headerKey(key)] = h
52
+ else
53
+ @headers[headerKey(key)] = [value.to_s]
54
+ end
55
+ end
56
+
57
+ def delete(key)
58
+ @headers.delete headerKey(key)
59
+ end
60
+ end
61
+
62
+
63
+ class Context
64
+
65
+ # FN_CALL_ID - a unique ID for each function execution.
15
66
  # FN_REQUEST_URL - the full URL for the request (parsing example)
67
+ # FN_HEADER_$X - the HTTP headers that were set for this request. Replace $X with the upper cased name of the header and replace dashes in the header with underscores.
68
+ # $X - any configuration values you've set for the Application or the Route. Replace X with the upper cased name of the config variable you set. Ex: minio_secret=secret will be exposed via MINIO_SECRET env var.
16
69
  # FN_APP_NAME - the name of the application that matched this route, eg: myapp
17
- # FN_PATH - the matched route, eg: /hello
18
70
  # FN_METHOD - the HTTP method for the request, eg: GET or POST
19
- # FN_CALL_ID - a unique ID for each function execution.
20
- # FN_FORMAT - a string representing one of the function formats, currently either default or http. Default is default.
21
71
  # FN_MEMORY - a number representing the amount of memory available to the call, in MB
22
- # FN_TYPE - the type for this call, currently 'sync' or 'async'
23
- # FN_HEADER_$X - the HTTP headers that were set for this request. Replace $X with the upper cased name of the header and replace dashes in the header with underscores.
24
- # $X - any configuration values you've set for the Application or the Route. Replace X with the upper cased name of the config variable you set. Ex: minio_secret=secret will be exposed via MINIO_SECRET env var.
25
- # FN_PARAM_$Y
26
72
 
27
- # CloudEvent format: https://github.com/cloudevents/spec/blob/master/serialization.md#json
28
73
 
29
- attr_reader :event
74
+ attr_reader :headers
75
+ attr_reader :response_headers
30
76
 
31
- def initialize(event)
32
- @event = event
77
+ def initialize(headers_in, headers_out)
78
+ @headers = headers_in
79
+ @response_headers = headers_out
80
+ @config ||= Config.new
33
81
  end
34
82
 
35
- # If it's a CNCF CloudEvent
36
- def cloud_event?
37
- ENV['FN_FORMAT'] == "cloudevent"
83
+
84
+ def call_id
85
+ @headers['fn-call-id']
38
86
  end
39
87
 
40
- def config
41
- @config ||= Config.new
88
+
89
+ def app_id
90
+ @config['FN_APP_ID']
42
91
  end
43
92
 
44
- def call_id
45
- cloud_event? ? event['eventID'] : event['call_id']
93
+
94
+ def fn_id
95
+ @config['FN_FN_ID']
96
+ end
97
+
98
+ def deadline
99
+ DateTime.iso8601(@headers['fn-deadline'])
100
+ end
101
+
102
+ def memory
103
+ @config['FN_MEMORY'].to_i
46
104
  end
47
105
 
48
106
  def content_type
49
- cloud_event? ? event['contentType'] : event['content_type']
107
+ @headers['content-type']
108
+ end
109
+
110
+ def http_context
111
+ HTTPContext.new(self)
112
+ end
113
+ end
114
+
115
+
116
+ class HTTPContext
117
+
118
+ attr_reader :headers
119
+ attr_reader :response_headers
120
+
121
+ def initialize(ctx)
122
+
123
+ @ctx = ctx
124
+
125
+
126
+ http_headers = {}
127
+ ctx.headers.each {|k, v|
128
+ if k.downcase.start_with?('fn-http-h-')
129
+ new_key = k['fn-http-h-'.length..k.length]
130
+ http_headers[new_key] = v
131
+ end
132
+ }
133
+
134
+ @headers = InHeaders.new(http_headers, nil)
135
+ @response_headers = OutHeaders.new(ctx.response_headers, lambda {|s| 'fn-http-h-' + s})
50
136
  end
51
137
 
52
- def protocol
53
- cloud_event? ? event['extensions']['protocol'] : event['protocol']
138
+
139
+ def request_url
140
+ @ctx.headers['fn-http-request-url']
54
141
  end
142
+
143
+ def method
144
+ @ctx.headers['fn-http-method']
145
+ end
146
+
147
+
148
+ def status_code=(val)
149
+ @ctx.response_headers['fn-http-status'] = val.to_i
150
+ end
151
+
55
152
  end
56
153
  end
154
+
data/lib/fdk/runner.rb CHANGED
@@ -1,75 +1,150 @@
1
+ require "webrick"
2
+ require "fileutils"
3
+ require "json"
4
+ require "set"
5
+
1
6
  # Looks for call(context, input) function
2
7
  # Executes it with input
3
8
  # Responds with output
9
+ module FDK
10
+ @filter_headers = Set["content-length", "te", "transfer-encoding",
11
+ "upgrade", "trailer"]
4
12
 
5
- require 'json'
6
- require 'yajl'
13
+ def self.check_format
14
+ f = ENV["FN_FORMAT"]
15
+ raise "'#{f}' not supported in Ruby FDK." unless f == "http-stream"
7
16
 
8
- module FDK
9
- def self.handle(function, input_stream: STDIN, output_stream: STDOUT)
10
- format = ENV['FN_FORMAT']
11
- if format == 'cloudevent'
12
- parser = Yajl::Parser.new
13
-
14
- parser.on_parse_complete = lambda do |event|
15
- context = Context.new(event)
16
- body = event['data']
17
- # Skipping json parsing of body because it would already be a parsed map according to the format spec defined here: https://github.com/cloudevents/spec/blob/master/serialization.md#json
18
- se = FDK.single_event(function: function, context: context, input: body)
19
-
20
- # Respond with modified event
21
- event['data'] = se
22
- event['extensions']['protocol'] = {
23
- headers: {
24
- 'Content-Type' => ['application/json']
25
- },
26
- 'status_code' => 200
27
- }
28
- output_stream.puts event.to_json
29
- output_stream.puts
30
- output_stream.flush
31
- end
17
+ f
18
+ end
19
+ private_class_method :check_format
20
+
21
+ def self.listener
22
+ l = ENV["FN_LISTENER"]
23
+ if l.nil? || !l.start_with?("unix:/")
24
+ raise "Missing or invalid socket URL in FN_LISTENER."
25
+ end
32
26
 
33
- input_stream.each_line { |line| parser.parse_chunk(line) }
27
+ l
28
+ end
29
+ private_class_method :listener
34
30
 
35
- elsif format == 'json'
36
- parser = Yajl::Parser.new
31
+ @dbg = ENV["FDK_DEBUG"]
37
32
 
38
- parser.on_parse_complete = lambda do |event|
39
- context = Context.new(event)
40
- body = event['body']
41
- if context.content_type == 'application/json' && body != ''
42
- body = Yajl::Parser.parse(body)
33
+ def self.debug(msg)
34
+ STDERR.puts(msg) if @dbg
35
+ end
36
+ private_class_method :debug
37
+
38
+ def self.handle(target:)
39
+ # To avoid Fn trying to connect to the socket before
40
+ # it's ready, the FDK creates a socket on (tmp_file).
41
+ #
42
+ # When the socket is ready to accept connections,
43
+ # the FDK links the tmp_file to the socket_file.
44
+ #
45
+ # Fn waits for the socket_file to be created and then connects
46
+ check_format
47
+ l = listener
48
+ socket_file = l[5..l.length]
49
+ tmp_file = socket_file + ".tmp"
50
+
51
+ debug tmp_file
52
+ debug socket_file
53
+ UNIXServer.open(tmp_file) do |serv|
54
+ File.chmod(0o666, tmp_file)
55
+ debug "listening on #{tmp_file}->#{socket_file}"
56
+ FileUtils.ln_s(File.basename(tmp_file), socket_file)
57
+
58
+ loop do
59
+ s = serv.accept
60
+ begin
61
+ loop do
62
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
63
+ req.parse s
64
+ debug "got request #{req}"
65
+ resp = WEBrick::HTTPResponse.new(WEBrick::Config::HTTP)
66
+ resp.status = 200
67
+ handle_call(target, req, resp)
68
+ resp.send_response s
69
+ debug "sending resp #{resp.status}, #{resp.header}"
70
+ break unless req.keep_alive?
71
+ end
72
+ rescue StandardError => e
73
+ STDERR.puts "Error in request handling #{e}"
74
+ STDERR.puts e.backtrace
43
75
  end
44
- se = FDK.single_event(function: function, context: context, input: body)
45
- response = {
46
- headers: {
47
- 'Content-Type' => ['application/json']
48
- },
49
- 'status_code' => 200,
50
- body: se.to_json
51
- }
52
- output_stream.puts response.to_json
53
- output_stream.puts
54
- output_stream.flush
76
+ s.close
55
77
  end
78
+ end
79
+ end
56
80
 
57
- input_stream.each_line { |line| parser.parse_chunk(line) }
81
+ def self.set_error(resp, error)
82
+ STDERR.puts "Error in function: \"#{error}\""
83
+ STDERR.puts error.backtrace
58
84
 
59
- elsif format == 'default'
60
- event = {}
61
- event['call_id'] = ENV['FN_CALL_ID']
62
- event['protocol'] = {
63
- 'type' => 'http',
64
- 'request_url' => ENV['FN_REQUEST_URL']
65
- }
66
- output_stream.puts FDK.single_event(function: function, context: Context.new(event), input: input_stream.read.chomp).to_json
85
+ resp["content-type"] = "application/json"
86
+ resp.status = 502
87
+ resp.body = { message: "An error occurred in the function",
88
+ detail: error.to_s }.to_json
89
+ end
90
+ private_class_method :set_error
91
+
92
+ def self.handle_call(target, req, resp)
93
+ headers = {}
94
+ req.header.map do |k, v|
95
+ headers[k] = v unless @filter_headers.include? k
96
+ end
97
+
98
+ headers_out_hash = {}
99
+ headers_out = FDK::OutHeaders.new(headers_out_hash, nil)
100
+ headers_in = FDK::InHeaders.new(headers, nil)
101
+ context = FDK::Context.new(headers_in, headers_out)
102
+ input = ParsedInput.new(raw_input: req.body.to_s)
103
+
104
+ begin
105
+ rv = if target.respond_to? :call
106
+ target.call(context: context, input: input.parsed)
107
+ else
108
+ send(target, context: context, input: input.parsed)
109
+ end
110
+ rescue StandardError => e
111
+ set_error(resp, e)
112
+ return
113
+ end
114
+
115
+ resp.status = 200
116
+ headers_out_hash.map do |k, v|
117
+ resp[k] = v.join(",") unless @filter_headers.include? k
118
+ end
119
+
120
+ # TODO: gimme a bit me flexibility on response handling
121
+ # binary, streams etc
122
+ if !rv.nil? && rv.respond_to?("to_json")
123
+ resp.body = rv.to_json
124
+ # don't override content type if already set
125
+ resp["content-type"] = "application/json" unless resp["content-type"]
67
126
  else
68
- raise "Format '#{format}' not supported in Ruby FDK."
127
+ resp.body = rv.to_s
69
128
  end
70
129
  end
71
130
 
72
- def self.single_event(function:, context:, input:)
73
- send(function, context: context, input: input)
131
+ # Stores raw input and can parse it as
132
+ # JSON (add extra formats as required)
133
+ class ParsedInput
134
+ attr_reader :raw
135
+
136
+ def initialize(raw_input:)
137
+ @raw = raw_input
138
+ end
139
+
140
+ def as_json
141
+ @json ||= JSON.parse(raw)
142
+ rescue JSON::ParserError
143
+ @json = false
144
+ end
145
+
146
+ def parsed
147
+ as_json || raw
148
+ end
74
149
  end
75
150
  end
data/lib/fdk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module FDK
2
- VERSION = "0.0.13"
2
+ VERSION = "0.0.14"
3
3
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.13
4
+ version: 0.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Travis Reeder
8
8
  - Ewan Slater
9
+ - Owen Cliffe
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2018-09-13 00:00:00.000000000 Z
13
+ date: 2018-10-05 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: json
@@ -32,35 +33,37 @@ dependencies:
32
33
  - !ruby/object:Gem::Version
33
34
  version: 2.1.0
34
35
  - !ruby/object:Gem::Dependency
35
- name: yajl-ruby
36
+ name: net_http_unix
36
37
  requirement: !ruby/object:Gem::Requirement
37
38
  requirements:
38
39
  - - "~>"
39
40
  - !ruby/object:Gem::Version
40
- version: '1.2'
41
+ version: '0.2'
41
42
  - - ">="
42
43
  - !ruby/object:Gem::Version
43
- version: 1.2.1
44
- type: :runtime
44
+ version: 0.2.1
45
+ type: :development
45
46
  prerelease: false
46
47
  version_requirements: !ruby/object:Gem::Requirement
47
48
  requirements:
48
49
  - - "~>"
49
50
  - !ruby/object:Gem::Version
50
- version: '1.2'
51
+ version: '0.2'
51
52
  - - ">="
52
53
  - !ruby/object:Gem::Version
53
- version: 1.2.1
54
+ version: 0.2.1
54
55
  description: Ruby Function Developer Kit for Fn Project.
55
56
  email:
56
57
  - treeder@gmail.com
57
58
  - ewan.slater@gmail.com
59
+ - owen.cliffe@oracle.com
58
60
  executables: []
59
61
  extensions: []
60
62
  extra_rdoc_files: []
61
63
  files:
62
64
  - LICENSE
63
65
  - README.md
66
+ - Rakefile
64
67
  - lib/fdk.rb
65
68
  - lib/fdk/context.rb
66
69
  - lib/fdk/runner.rb
@@ -77,7 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
80
  requirements:
78
81
  - - ">="
79
82
  - !ruby/object:Gem::Version
80
- version: '2.0'
83
+ version: '2.4'
81
84
  required_rubygems_version: !ruby/object:Gem::Requirement
82
85
  requirements:
83
86
  - - ">="
@@ -85,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
88
  version: '0'
86
89
  requirements: []
87
90
  rubyforge_project:
88
- rubygems_version: 2.6.14
91
+ rubygems_version: 2.7.6
89
92
  signing_key:
90
93
  specification_version: 4
91
94
  summary: Ruby FDK for Fn Project