fdk 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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