functions_framework 0.4.0 → 0.6.0
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 +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +9 -9
- data/bin/functions-framework +1 -1
- data/bin/functions-framework-ruby +1 -1
- data/docs/deploying-functions.md +29 -14
- data/docs/overview.md +4 -4
- data/docs/testing-functions.md +9 -11
- data/docs/writing-functions.md +73 -13
- data/lib/functions_framework.rb +35 -7
- data/lib/functions_framework/cli.rb +97 -22
- data/lib/functions_framework/function.rb +3 -2
- data/lib/functions_framework/legacy_event_converter.rb +28 -29
- data/lib/functions_framework/registry.rb +40 -6
- data/lib/functions_framework/server.rb +14 -10
- data/lib/functions_framework/testing.rb +20 -8
- data/lib/functions_framework/version.rb +1 -1
- metadata +22 -108
- data/lib/functions_framework/cloud_events.rb +0 -45
- data/lib/functions_framework/cloud_events/content_type.rb +0 -216
- data/lib/functions_framework/cloud_events/errors.rb +0 -42
- data/lib/functions_framework/cloud_events/event.rb +0 -84
- data/lib/functions_framework/cloud_events/event/field_interpreter.rb +0 -150
- data/lib/functions_framework/cloud_events/event/v0.rb +0 -236
- data/lib/functions_framework/cloud_events/event/v1.rb +0 -223
- data/lib/functions_framework/cloud_events/http_binding.rb +0 -310
- data/lib/functions_framework/cloud_events/json_format.rb +0 -173
@@ -42,8 +42,31 @@ module FunctionsFramework
|
|
42
42
|
@detailed_errors = nil
|
43
43
|
@signature_type = ::ENV["FUNCTION_SIGNATURE_TYPE"]
|
44
44
|
@logging_level = init_logging_level
|
45
|
+
@what_to_do = nil
|
46
|
+
@error_message = nil
|
47
|
+
@exit_code = 0
|
45
48
|
end
|
46
49
|
|
50
|
+
##
|
51
|
+
# Determine if an error has occurred
|
52
|
+
#
|
53
|
+
# @return [boolean]
|
54
|
+
#
|
55
|
+
def error?
|
56
|
+
!@error_message.nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# @return [Integer] The current exit status.
|
61
|
+
#
|
62
|
+
attr_reader :exit_code
|
63
|
+
|
64
|
+
##
|
65
|
+
# @return [String] The current error message.
|
66
|
+
# @return [nil] if no error has occurred.
|
67
|
+
#
|
68
|
+
attr_reader :error_message
|
69
|
+
|
47
70
|
##
|
48
71
|
# Parse the given command line arguments.
|
49
72
|
# Exits if argument parsing failed.
|
@@ -52,7 +75,7 @@ module FunctionsFramework
|
|
52
75
|
# @return [self]
|
53
76
|
#
|
54
77
|
def parse_args argv # rubocop:disable Metrics/MethodLength
|
55
|
-
option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
|
78
|
+
@option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
|
56
79
|
op.on "-t", "--target TARGET",
|
57
80
|
"Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
|
58
81
|
@target = val
|
@@ -84,55 +107,92 @@ module FunctionsFramework
|
|
84
107
|
op.on "--[no-]detailed-errors", "Set whether to show error details" do |val|
|
85
108
|
@detailed_errors = val
|
86
109
|
end
|
110
|
+
op.on "--verify", "Verify the app only, but do not run the server." do
|
111
|
+
@what_to_do ||= :verify
|
112
|
+
end
|
87
113
|
op.on "-v", "--verbose", "Increase log verbosity" do
|
88
114
|
@logging_level -= 1
|
89
115
|
end
|
90
116
|
op.on "-q", "--quiet", "Decrease log verbosity" do
|
91
117
|
@logging_level += 1
|
92
118
|
end
|
119
|
+
op.on "--version", "Display the framework version" do
|
120
|
+
@what_to_do ||= :version
|
121
|
+
end
|
93
122
|
op.on "--help", "Display help" do
|
94
|
-
|
95
|
-
exit
|
123
|
+
@what_to_do ||= :help
|
96
124
|
end
|
97
125
|
end
|
98
|
-
|
99
|
-
|
126
|
+
begin
|
127
|
+
@option_parser.parse! argv
|
128
|
+
error! "Unrecognized arguments: #{argv}\n#{@option_parser}", 2 unless argv.empty?
|
129
|
+
rescue ::OptionParser::ParseError => e
|
130
|
+
error! "#{e.message}\n#{@option_parser}", 2
|
131
|
+
end
|
100
132
|
self
|
101
133
|
end
|
102
134
|
|
103
135
|
##
|
104
|
-
#
|
105
|
-
#
|
136
|
+
# Perform the requested function.
|
137
|
+
#
|
138
|
+
# * If the `--version` flag was given, display the version.
|
139
|
+
# * If the `--help` flag was given, display online help.
|
140
|
+
# * If the `--verify` flag was given, load and verify the function,
|
141
|
+
# displaying any errors, then exit without starting a server.
|
142
|
+
# * Otherwise, start the configured server and block until it stops.
|
106
143
|
#
|
107
144
|
# @return [self]
|
108
145
|
#
|
109
146
|
def run
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
147
|
+
return self if error?
|
148
|
+
case @what_to_do
|
149
|
+
when :version
|
150
|
+
puts ::FunctionsFramework::VERSION
|
151
|
+
when :help
|
152
|
+
puts @option_parser
|
153
|
+
when :verify
|
154
|
+
begin
|
155
|
+
load_function
|
156
|
+
puts "OK"
|
157
|
+
rescue ::StandardError => e
|
158
|
+
error! e.message
|
159
|
+
end
|
160
|
+
else
|
161
|
+
begin
|
162
|
+
start_server.wait_until_stopped
|
163
|
+
rescue ::StandardError => e
|
164
|
+
error! e.message
|
165
|
+
end
|
114
166
|
end
|
115
|
-
server.wait_until_stopped
|
116
167
|
self
|
117
168
|
end
|
118
169
|
|
119
170
|
##
|
120
|
-
#
|
171
|
+
# Finish the CLI, displaying any error status and exiting with the current
|
172
|
+
# exit code. Never returns.
|
173
|
+
#
|
174
|
+
def complete
|
175
|
+
warn @error_message if @error_message
|
176
|
+
exit @exit_code
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Load the source and get and verify the requested function.
|
121
181
|
# If a validation error occurs, raise an exception.
|
122
|
-
# This is used for testing the CLI.
|
123
182
|
#
|
124
|
-
# @return [FunctionsFramework::
|
183
|
+
# @return [FunctionsFramework::Function]
|
125
184
|
#
|
126
185
|
# @private
|
127
186
|
#
|
128
|
-
def
|
187
|
+
def load_function
|
129
188
|
::FunctionsFramework.logger.level = @logging_level
|
130
|
-
::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION}
|
189
|
+
::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION}"
|
131
190
|
::ENV["FUNCTION_TARGET"] = @target
|
132
191
|
::ENV["FUNCTION_SOURCE"] = @source
|
133
192
|
::ENV["FUNCTION_SIGNATURE_TYPE"] = @signature_type
|
134
193
|
::FunctionsFramework.logger.info "FunctionsFramework: Loading functions from #{@source.inspect}..."
|
135
194
|
load @source
|
195
|
+
::FunctionsFramework.logger.info "FunctionsFramework: Looking for function name #{@target.inspect}..."
|
136
196
|
function = ::FunctionsFramework.global_registry[@target]
|
137
197
|
raise "Undefined function: #{@target.inspect}" if function.nil?
|
138
198
|
unless @signature_type.nil? ||
|
@@ -140,6 +200,20 @@ module FunctionsFramework
|
|
140
200
|
["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event
|
141
201
|
raise "Function #{@target.inspect} does not match type #{@signature_type}"
|
142
202
|
end
|
203
|
+
function
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Start the configured server and return the running server object.
|
208
|
+
# If a validation error occurs, raise an exception.
|
209
|
+
#
|
210
|
+
# @return [FunctionsFramework::Server]
|
211
|
+
#
|
212
|
+
# @private
|
213
|
+
#
|
214
|
+
def start_server
|
215
|
+
function = load_function
|
216
|
+
::FunctionsFramework.logger.info "FunctionsFramework: Starting server..."
|
143
217
|
::FunctionsFramework.start function do |config|
|
144
218
|
config.rack_env = @env
|
145
219
|
config.port = @port
|
@@ -160,12 +234,13 @@ module FunctionsFramework
|
|
160
234
|
end
|
161
235
|
|
162
236
|
##
|
163
|
-
#
|
164
|
-
# @param message [String]
|
237
|
+
# Set the error status.
|
238
|
+
# @param message [String] Error message.
|
239
|
+
# @param code [Integer] Exit code, defaults to 1.
|
165
240
|
#
|
166
|
-
def error message
|
167
|
-
|
168
|
-
|
241
|
+
def error! message, code = 1
|
242
|
+
@error_message = message
|
243
|
+
@exit_code = code
|
169
244
|
end
|
170
245
|
end
|
171
246
|
end
|
@@ -23,8 +23,9 @@ module FunctionsFramework
|
|
23
23
|
# `Rack::Request` argument and returns one of various HTTP response types.
|
24
24
|
# See {FunctionsFramework::Registry.add_http}. For a function of type
|
25
25
|
# `:cloud_event`, the `call` method takes a single
|
26
|
-
#
|
27
|
-
# return a value.
|
26
|
+
# [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event)
|
27
|
+
# argument, and does not return a value.
|
28
|
+
# See {FunctionsFramework::Registry.add_cloud_event}.
|
28
29
|
#
|
29
30
|
# If a callable object is provided directly, its `call` method is invoked for
|
30
31
|
# every function execution. Note that this means it may be called multiple
|
@@ -23,19 +23,17 @@ module FunctionsFramework
|
|
23
23
|
# Decode an event from the given Rack environment hash.
|
24
24
|
#
|
25
25
|
# @param env [Hash] The Rack environment
|
26
|
-
# @return [
|
27
|
-
# be converted
|
26
|
+
# @return [::CloudEvents::Event] if the request could be converted
|
28
27
|
# @return [nil] if the event format was not recognized.
|
29
28
|
#
|
30
29
|
def decode_rack_env env
|
31
|
-
content_type = CloudEvents::ContentType.new env["CONTENT_TYPE"]
|
30
|
+
content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"]
|
32
31
|
return nil unless content_type.media_type == "application" && content_type.subtype_base == "json"
|
33
32
|
input = read_input_json env["rack.input"], content_type.charset
|
34
33
|
return nil unless input
|
35
|
-
|
36
|
-
context = normalized_context raw_context
|
34
|
+
context = normalized_context input
|
37
35
|
return nil unless context
|
38
|
-
construct_cloud_event context, input["data"]
|
36
|
+
construct_cloud_event context, input["data"], content_type.charset
|
39
37
|
end
|
40
38
|
|
41
39
|
private
|
@@ -50,27 +48,27 @@ module FunctionsFramework
|
|
50
48
|
nil
|
51
49
|
end
|
52
50
|
|
53
|
-
def normalized_context
|
54
|
-
|
55
|
-
|
56
|
-
timestamp = raw_context["timestamp"]
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
return nil unless service && resource
|
51
|
+
def normalized_context input
|
52
|
+
raw_context = input["context"]
|
53
|
+
id = raw_context&.[]("eventId") || input["eventId"]
|
54
|
+
timestamp = raw_context&.[]("timestamp") || input["timestamp"]
|
55
|
+
type = raw_context&.[]("eventType") || input["eventType"]
|
56
|
+
service, resource = analyze_resource raw_context&.[]("resource") || input["resource"]
|
57
|
+
service ||= service_from_type type
|
58
|
+
return nil unless id && timestamp && type && service && resource
|
62
59
|
{ id: id, timestamp: timestamp, type: type, service: service, resource: resource }
|
63
60
|
end
|
64
61
|
|
65
|
-
def analyze_resource raw_resource
|
62
|
+
def analyze_resource raw_resource
|
63
|
+
service = resource = nil
|
66
64
|
case raw_resource
|
67
65
|
when ::Hash
|
68
|
-
|
66
|
+
service = raw_resource["service"]
|
67
|
+
resource = raw_resource["name"]
|
69
68
|
when ::String
|
70
|
-
|
71
|
-
else
|
72
|
-
[nil, nil]
|
69
|
+
resource = raw_resource
|
73
70
|
end
|
71
|
+
[service, resource]
|
74
72
|
end
|
75
73
|
|
76
74
|
def service_from_type type
|
@@ -80,19 +78,20 @@ module FunctionsFramework
|
|
80
78
|
nil
|
81
79
|
end
|
82
80
|
|
83
|
-
def construct_cloud_event context, data
|
81
|
+
def construct_cloud_event context, data, charset
|
84
82
|
source, subject = convert_source context[:service], context[:resource]
|
85
83
|
type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
|
86
84
|
return nil unless type && source
|
87
85
|
ce_data = convert_data context[:service], data
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
86
|
+
content_type = "application/json; charset=#{charset}"
|
87
|
+
::CloudEvents::Event.new id: context[:id],
|
88
|
+
source: source,
|
89
|
+
type: type,
|
90
|
+
spec_version: "1.0",
|
91
|
+
data_content_type: content_type,
|
92
|
+
data: ce_data,
|
93
|
+
subject: subject,
|
94
|
+
time: context[:timestamp]
|
96
95
|
end
|
97
96
|
|
98
97
|
def convert_source service, resource
|
@@ -25,8 +25,9 @@ module FunctionsFramework
|
|
25
25
|
# Create a new empty registry.
|
26
26
|
#
|
27
27
|
def initialize
|
28
|
-
|
28
|
+
@mutex = ::Monitor.new
|
29
29
|
@functions = {}
|
30
|
+
@start_tasks = []
|
30
31
|
end
|
31
32
|
|
32
33
|
##
|
@@ -37,7 +38,7 @@ module FunctionsFramework
|
|
37
38
|
# @return [nil] if the function is not found
|
38
39
|
#
|
39
40
|
def [] name
|
40
|
-
@functions[name.to_s]
|
41
|
+
@mutex.synchronize { @functions[name.to_s] }
|
41
42
|
end
|
42
43
|
|
43
44
|
##
|
@@ -46,7 +47,21 @@ module FunctionsFramework
|
|
46
47
|
# @return [Array<String>]
|
47
48
|
#
|
48
49
|
def names
|
49
|
-
@functions.keys.sort
|
50
|
+
@mutex.synchronize { @functions.keys.sort }
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Run all startup tasks.
|
55
|
+
#
|
56
|
+
# @param server [FunctionsFramework::Server] The server that is starting.
|
57
|
+
# @return [self]
|
58
|
+
#
|
59
|
+
def run_startup_tasks server
|
60
|
+
tasks = @mutex.synchronize { @start_tasks.dup }
|
61
|
+
tasks.each do |task|
|
62
|
+
task.call server.function, server.config
|
63
|
+
end
|
64
|
+
self
|
50
65
|
end
|
51
66
|
|
52
67
|
##
|
@@ -68,7 +83,7 @@ module FunctionsFramework
|
|
68
83
|
#
|
69
84
|
def add_http name, &block
|
70
85
|
name = name.to_s
|
71
|
-
synchronize do
|
86
|
+
@mutex.synchronize do
|
72
87
|
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
73
88
|
@functions[name] = Function.new name, :http, &block
|
74
89
|
end
|
@@ -80,7 +95,8 @@ module FunctionsFramework
|
|
80
95
|
#
|
81
96
|
# You must provide a name for the function, and a block that implemets the
|
82
97
|
# function. The block should take _one_ argument: the event object of type
|
83
|
-
#
|
98
|
+
# [`CloudEvents::Event`](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event).
|
99
|
+
# Any return value is ignored.
|
84
100
|
#
|
85
101
|
# @param name [String] The function name
|
86
102
|
# @param block [Proc] The function code as a proc
|
@@ -88,11 +104,29 @@ module FunctionsFramework
|
|
88
104
|
#
|
89
105
|
def add_cloud_event name, &block
|
90
106
|
name = name.to_s
|
91
|
-
synchronize do
|
107
|
+
@mutex.synchronize do
|
92
108
|
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
93
109
|
@functions[name] = Function.new name, :cloud_event, &block
|
94
110
|
end
|
95
111
|
self
|
96
112
|
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Add a startup task.
|
116
|
+
#
|
117
|
+
# Startup tasks are generally run just before a server starts. They are
|
118
|
+
# passed two arguments: the {FunctionsFramework::Function} identifying the
|
119
|
+
# function to execute, and the {FunctionsFramework::Server::Config}
|
120
|
+
# specifying the (frozen) server configuration. Tasks have no return value.
|
121
|
+
#
|
122
|
+
# @param block [Proc] The startup task
|
123
|
+
# @return [self]
|
124
|
+
#
|
125
|
+
def add_startup_task &block
|
126
|
+
@mutex.synchronize do
|
127
|
+
@start_tasks << block
|
128
|
+
end
|
129
|
+
self
|
130
|
+
end
|
97
131
|
end
|
98
132
|
end
|
@@ -82,9 +82,9 @@ module FunctionsFramework
|
|
82
82
|
@server.max_threads = @config.max_threads
|
83
83
|
@server.leak_stack_on_error = @config.show_error_details?
|
84
84
|
@server.binder.add_tcp_listener @config.bind_addr, @config.port
|
85
|
-
@server.run true
|
86
85
|
@config.logger.info "FunctionsFramework: Serving function #{@function.name.inspect}" \
|
87
86
|
" on port #{@config.port}..."
|
87
|
+
@server.run true
|
88
88
|
end
|
89
89
|
end
|
90
90
|
self
|
@@ -149,8 +149,12 @@ module FunctionsFramework
|
|
149
149
|
::Signal.trap "SIGINT" do
|
150
150
|
Server.signal_enqueue "SIGINT", @config.logger, @server
|
151
151
|
end
|
152
|
-
|
153
|
-
|
152
|
+
begin
|
153
|
+
::Signal.trap "SIGHUP" do
|
154
|
+
Server.signal_enqueue "SIGHUP", @config.logger, @server
|
155
|
+
end
|
156
|
+
rescue ::ArgumentError # rubocop:disable Lint/HandleExceptions
|
157
|
+
# Not available on all systems
|
154
158
|
end
|
155
159
|
@signals_installed = true
|
156
160
|
end
|
@@ -340,7 +344,7 @@ module FunctionsFramework
|
|
340
344
|
string_response response, "text/plain", 200
|
341
345
|
when ::Hash
|
342
346
|
string_response ::JSON.dump(response), "application/json", 200
|
343
|
-
when CloudEvents::CloudEventsError
|
347
|
+
when ::CloudEvents::CloudEventsError
|
344
348
|
cloud_events_error_response response
|
345
349
|
when ::StandardError
|
346
350
|
error_response "#{response.class}: #{response.message}\n#{response.backtrace}\n"
|
@@ -401,7 +405,7 @@ module FunctionsFramework
|
|
401
405
|
def initialize function, config
|
402
406
|
super config
|
403
407
|
@function = function
|
404
|
-
@cloud_events = CloudEvents::HttpBinding.default
|
408
|
+
@cloud_events = ::CloudEvents::HttpBinding.default
|
405
409
|
@legacy_events = LegacyEventConverter.new
|
406
410
|
end
|
407
411
|
|
@@ -411,11 +415,11 @@ module FunctionsFramework
|
|
411
415
|
event = decode_event env
|
412
416
|
response =
|
413
417
|
case event
|
414
|
-
when CloudEvents::Event
|
418
|
+
when ::CloudEvents::Event
|
415
419
|
handle_cloud_event event, logger
|
416
420
|
when ::Array
|
417
|
-
CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
|
418
|
-
when CloudEvents::CloudEventsError
|
421
|
+
::CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
|
422
|
+
when ::CloudEvents::CloudEventsError
|
419
423
|
event
|
420
424
|
else
|
421
425
|
raise "Unexpected event type: #{event.class}"
|
@@ -428,8 +432,8 @@ module FunctionsFramework
|
|
428
432
|
def decode_event env
|
429
433
|
@cloud_events.decode_rack_env(env) ||
|
430
434
|
@legacy_events.decode_rack_env(env) ||
|
431
|
-
raise(CloudEvents::HttpContentError, "Unrecognized event format")
|
432
|
-
rescue CloudEvents::CloudEventsError => e
|
435
|
+
raise(::CloudEvents::HttpContentError, "Unrecognized event format")
|
436
|
+
rescue ::CloudEvents::CloudEventsError => e
|
433
437
|
e
|
434
438
|
end
|
435
439
|
|