functions_framework 0.5.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +5 -5
- data/bin/functions-framework +4 -1
- data/bin/functions-framework-ruby +1 -1
- data/docs/deploying-functions.md +7 -11
- data/docs/overview.md +4 -4
- data/docs/testing-functions.md +50 -0
- data/docs/writing-functions.md +246 -7
- data/lib/functions_framework.rb +30 -3
- data/lib/functions_framework/cli.rb +98 -23
- data/lib/functions_framework/function.rb +190 -48
- data/lib/functions_framework/legacy_event_converter.rb +49 -22
- data/lib/functions_framework/registry.rb +34 -11
- data/lib/functions_framework/server.rb +22 -17
- data/lib/functions_framework/testing.rb +106 -18
- data/lib/functions_framework/version.rb +1 -1
- metadata +8 -8
@@ -33,7 +33,7 @@ module FunctionsFramework
|
|
33
33
|
return nil unless input
|
34
34
|
context = normalized_context input
|
35
35
|
return nil unless context
|
36
|
-
construct_cloud_event context, input["data"]
|
36
|
+
construct_cloud_event context, input["data"]
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
@@ -49,16 +49,19 @@ module FunctionsFramework
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def normalized_context input
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
service, resource = analyze_resource raw_context&.[]("resource") || input["resource"]
|
52
|
+
id = normalized_context_field input, "eventId"
|
53
|
+
timestamp = normalized_context_field input, "timestamp"
|
54
|
+
type = normalized_context_field input, "eventType"
|
55
|
+
service, resource = analyze_resource normalized_context_field input, "resource"
|
57
56
|
service ||= service_from_type type
|
58
57
|
return nil unless id && timestamp && type && service && resource
|
59
58
|
{ id: id, timestamp: timestamp, type: type, service: service, resource: resource }
|
60
59
|
end
|
61
60
|
|
61
|
+
def normalized_context_field input, field
|
62
|
+
input["context"]&.[](field) || input[field]
|
63
|
+
end
|
64
|
+
|
62
65
|
def analyze_resource raw_resource
|
63
66
|
service = resource = nil
|
64
67
|
case raw_resource
|
@@ -78,37 +81,47 @@ module FunctionsFramework
|
|
78
81
|
nil
|
79
82
|
end
|
80
83
|
|
81
|
-
def construct_cloud_event context, data
|
84
|
+
def construct_cloud_event context, data
|
82
85
|
source, subject = convert_source context[:service], context[:resource]
|
83
86
|
type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
|
84
87
|
return nil unless type && source
|
85
|
-
ce_data = convert_data context[:service], data
|
86
|
-
content_type = "application/json
|
88
|
+
ce_data, data_subject = convert_data context[:service], data
|
89
|
+
content_type = "application/json"
|
87
90
|
::CloudEvents::Event.new id: context[:id],
|
88
91
|
source: source,
|
89
92
|
type: type,
|
90
93
|
spec_version: "1.0",
|
91
94
|
data_content_type: content_type,
|
92
95
|
data: ce_data,
|
93
|
-
subject: subject,
|
96
|
+
subject: subject || data_subject,
|
94
97
|
time: context[:timestamp]
|
95
98
|
end
|
96
99
|
|
97
100
|
def convert_source service, resource
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
["//#{service}/#{resource}", nil]
|
104
|
-
end
|
101
|
+
return ["//#{service}/#{resource}", nil] unless CE_SERVICE_TO_RESOURCE_RE.key? service
|
102
|
+
|
103
|
+
match = CE_SERVICE_TO_RESOURCE_RE[service].match resource
|
104
|
+
return [nil, nil] unless match
|
105
|
+
["//#{service}/#{match[1]}", match[2]]
|
105
106
|
end
|
106
107
|
|
107
108
|
def convert_data service, data
|
108
|
-
|
109
|
-
|
109
|
+
case service
|
110
|
+
when "pubsub.googleapis.com"
|
111
|
+
[{ "message" => data }, nil]
|
112
|
+
when "firebaseauth.googleapis.com"
|
113
|
+
if data.key? "metadata"
|
114
|
+
FIREBASE_AUTH_METADATA_LEGACY_TO_CE.each do |old_key, new_key|
|
115
|
+
if data["metadata"].key? old_key
|
116
|
+
data["metadata"][new_key] = data["metadata"][old_key]
|
117
|
+
data["metadata"].delete old_key
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
subject = "users/#{data['uid']}" if data.key? "uid"
|
122
|
+
[data, subject]
|
110
123
|
else
|
111
|
-
data
|
124
|
+
[data, nil]
|
112
125
|
end
|
113
126
|
end
|
114
127
|
|
@@ -116,8 +129,9 @@ module FunctionsFramework
|
|
116
129
|
%r{^providers/cloud\.firestore/} => "firestore.googleapis.com",
|
117
130
|
%r{^providers/cloud\.pubsub/} => "pubsub.googleapis.com",
|
118
131
|
%r{^providers/cloud\.storage/} => "storage.googleapis.com",
|
119
|
-
%r{^providers/firebase\.auth/} => "
|
120
|
-
%r{^providers/google\.firebase}
|
132
|
+
%r{^providers/firebase\.auth/} => "firebaseauth.googleapis.com",
|
133
|
+
%r{^providers/google\.firebase\.analytics/} => "firebase.googleapis.com",
|
134
|
+
%r{^providers/google\.firebase\.database/} => "firebasedatabase.googleapis.com"
|
121
135
|
}.freeze
|
122
136
|
|
123
137
|
LEGACY_TYPE_TO_CE_TYPE = {
|
@@ -140,5 +154,18 @@ module FunctionsFramework
|
|
140
154
|
"providers/google.firebase.database/eventTypes/ref.delete" => "google.firebase.database.document.v1.deleted",
|
141
155
|
"providers/cloud.storage/eventTypes/object.change" => "google.cloud.storage.object.v1.finalized"
|
142
156
|
}.freeze
|
157
|
+
|
158
|
+
CE_SERVICE_TO_RESOURCE_RE = {
|
159
|
+
"firebase.googleapis.com" => %r{^(projects/[^/]+)/(events/[^/]+)$},
|
160
|
+
"firebasedatabase.googleapis.com" => %r{^(projects/_/instances/[^/]+)/(refs/.+)$},
|
161
|
+
"firestore.googleapis.com" => %r{^(projects/[^/]+/databases/\(default\))/(documents/.+)$},
|
162
|
+
"storage.googleapis.com" => %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}
|
163
|
+
}.freeze
|
164
|
+
|
165
|
+
# Map Firebase Auth legacy event metadata field names to their equivalent CloudEvent field names.
|
166
|
+
FIREBASE_AUTH_METADATA_LEGACY_TO_CE = {
|
167
|
+
"createdAt" => "createTime",
|
168
|
+
"lastSignedInAt" => "lastSignInTime"
|
169
|
+
}.freeze
|
143
170
|
end
|
144
171
|
end
|
@@ -12,21 +12,18 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
require "monitor"
|
16
|
-
|
17
15
|
module FunctionsFramework
|
18
16
|
##
|
19
17
|
# Registry providing lookup of functions by name.
|
20
18
|
#
|
21
19
|
class Registry
|
22
|
-
include ::MonitorMixin
|
23
|
-
|
24
20
|
##
|
25
21
|
# Create a new empty registry.
|
26
22
|
#
|
27
23
|
def initialize
|
28
|
-
|
24
|
+
@mutex = ::Mutex.new
|
29
25
|
@functions = {}
|
26
|
+
@start_tasks = []
|
30
27
|
end
|
31
28
|
|
32
29
|
##
|
@@ -37,7 +34,7 @@ module FunctionsFramework
|
|
37
34
|
# @return [nil] if the function is not found
|
38
35
|
#
|
39
36
|
def [] name
|
40
|
-
@functions[name.to_s]
|
37
|
+
@mutex.synchronize { @functions[name.to_s] }
|
41
38
|
end
|
42
39
|
|
43
40
|
##
|
@@ -46,7 +43,16 @@ module FunctionsFramework
|
|
46
43
|
# @return [Array<String>]
|
47
44
|
#
|
48
45
|
def names
|
49
|
-
@functions.keys.sort
|
46
|
+
@mutex.synchronize { @functions.keys.sort }
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Return an array of startup tasks.
|
51
|
+
#
|
52
|
+
# @return [Array<FunctionsFramework::Function>]
|
53
|
+
#
|
54
|
+
def startup_tasks
|
55
|
+
@mutex.synchronize { @start_tasks.dup }
|
50
56
|
end
|
51
57
|
|
52
58
|
##
|
@@ -68,9 +74,9 @@ module FunctionsFramework
|
|
68
74
|
#
|
69
75
|
def add_http name, &block
|
70
76
|
name = name.to_s
|
71
|
-
synchronize do
|
77
|
+
@mutex.synchronize do
|
72
78
|
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
73
|
-
@functions[name] = Function.
|
79
|
+
@functions[name] = Function.http name, &block
|
74
80
|
end
|
75
81
|
self
|
76
82
|
end
|
@@ -89,9 +95,26 @@ module FunctionsFramework
|
|
89
95
|
#
|
90
96
|
def add_cloud_event name, &block
|
91
97
|
name = name.to_s
|
92
|
-
synchronize do
|
98
|
+
@mutex.synchronize do
|
93
99
|
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
94
|
-
@functions[name] = Function.
|
100
|
+
@functions[name] = Function.cloud_event name, &block
|
101
|
+
end
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Add a startup task.
|
107
|
+
#
|
108
|
+
# Startup tasks are generally run just before a server starts. They are
|
109
|
+
# passed the {FunctionsFramework::Function} identifying the function to
|
110
|
+
# execute, and have no return value.
|
111
|
+
#
|
112
|
+
# @param block [Proc] The startup task
|
113
|
+
# @return [self]
|
114
|
+
#
|
115
|
+
def add_startup_task &block
|
116
|
+
@mutex.synchronize do
|
117
|
+
@start_tasks << Function.startup_task(&block)
|
95
118
|
end
|
96
119
|
self
|
97
120
|
end
|
@@ -27,17 +27,22 @@ module FunctionsFramework
|
|
27
27
|
include ::MonitorMixin
|
28
28
|
|
29
29
|
##
|
30
|
-
# Create a new web server given a function
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
30
|
+
# Create a new web server given a function definition, a set of application
|
31
|
+
# globals, and server configuration.
|
32
|
+
#
|
33
|
+
# To configure the server, pass a block that takes a
|
34
|
+
# {FunctionsFramework::Server::Config} object as the parameter. This block
|
35
|
+
# is the only opportunity to modify the configuration; once the server is
|
36
|
+
# initialized, configuration is frozen.
|
35
37
|
#
|
36
38
|
# @param function [FunctionsFramework::Function] The function to execute.
|
39
|
+
# @param globals [Hash] Globals to pass to invocations. This hash should
|
40
|
+
# normally be frozen so separate function invocations cannot interfere
|
41
|
+
# with one another's globals.
|
37
42
|
# @yield [FunctionsFramework::Server::Config] A config object that can be
|
38
43
|
# manipulated to configure this server.
|
39
44
|
#
|
40
|
-
def initialize function
|
45
|
+
def initialize function, globals
|
41
46
|
super()
|
42
47
|
@config = Config.new
|
43
48
|
yield @config if block_given?
|
@@ -46,9 +51,9 @@ module FunctionsFramework
|
|
46
51
|
@app =
|
47
52
|
case function.type
|
48
53
|
when :http
|
49
|
-
HttpApp.new function, @config
|
54
|
+
HttpApp.new function, globals, @config
|
50
55
|
when :cloud_event
|
51
|
-
EventApp.new function, @config
|
56
|
+
EventApp.new function, globals, @config
|
52
57
|
else
|
53
58
|
raise "Unrecognized function type: #{function.type}"
|
54
59
|
end
|
@@ -82,9 +87,9 @@ module FunctionsFramework
|
|
82
87
|
@server.max_threads = @config.max_threads
|
83
88
|
@server.leak_stack_on_error = @config.show_error_details?
|
84
89
|
@server.binder.add_tcp_listener @config.bind_addr, @config.port
|
85
|
-
@server.run true
|
86
90
|
@config.logger.info "FunctionsFramework: Serving function #{@function.name.inspect}" \
|
87
91
|
" on port #{@config.port}..."
|
92
|
+
@server.run true
|
88
93
|
end
|
89
94
|
end
|
90
95
|
self
|
@@ -153,7 +158,7 @@ module FunctionsFramework
|
|
153
158
|
::Signal.trap "SIGHUP" do
|
154
159
|
Server.signal_enqueue "SIGHUP", @config.logger, @server
|
155
160
|
end
|
156
|
-
rescue ::ArgumentError
|
161
|
+
rescue ::ArgumentError
|
157
162
|
# Not available on all systems
|
158
163
|
end
|
159
164
|
@signals_installed = true
|
@@ -301,7 +306,7 @@ module FunctionsFramework
|
|
301
306
|
# @return [Integer]
|
302
307
|
#
|
303
308
|
def max_threads
|
304
|
-
@max_threads ||
|
309
|
+
@max_threads || 1
|
305
310
|
end
|
306
311
|
|
307
312
|
##
|
@@ -379,9 +384,10 @@ module FunctionsFramework
|
|
379
384
|
|
380
385
|
## @private
|
381
386
|
class HttpApp < AppBase
|
382
|
-
def initialize function, config
|
387
|
+
def initialize function, globals, config
|
383
388
|
super config
|
384
389
|
@function = function
|
390
|
+
@globals = globals
|
385
391
|
end
|
386
392
|
|
387
393
|
def call env
|
@@ -391,8 +397,7 @@ module FunctionsFramework
|
|
391
397
|
logger = env["rack.logger"] ||= @config.logger
|
392
398
|
request = ::Rack::Request.new env
|
393
399
|
logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
|
394
|
-
|
395
|
-
calling_context.call request
|
400
|
+
@function.call request, globals: @globals, logger: logger
|
396
401
|
rescue ::StandardError => e
|
397
402
|
e
|
398
403
|
end
|
@@ -402,9 +407,10 @@ module FunctionsFramework
|
|
402
407
|
|
403
408
|
## @private
|
404
409
|
class EventApp < AppBase
|
405
|
-
def initialize function, config
|
410
|
+
def initialize function, globals, config
|
406
411
|
super config
|
407
412
|
@function = function
|
413
|
+
@globals = globals
|
408
414
|
@cloud_events = ::CloudEvents::HttpBinding.default
|
409
415
|
@legacy_events = LegacyEventConverter.new
|
410
416
|
end
|
@@ -439,8 +445,7 @@ module FunctionsFramework
|
|
439
445
|
|
440
446
|
def handle_cloud_event event, logger
|
441
447
|
logger.info "FunctionsFramework: Handling CloudEvent"
|
442
|
-
|
443
|
-
calling_context.call event
|
448
|
+
@function.call event, globals: @globals, logger: logger
|
444
449
|
"ok"
|
445
450
|
rescue ::StandardError => e
|
446
451
|
e
|
@@ -75,19 +75,71 @@ module FunctionsFramework
|
|
75
75
|
Testing.load_for_testing path, &block
|
76
76
|
end
|
77
77
|
|
78
|
+
##
|
79
|
+
# Run startup tasks for the given function name and return the initialized
|
80
|
+
# globals hash.
|
81
|
+
#
|
82
|
+
# Normally, this will be run automatically prior to the first call to the
|
83
|
+
# function using {call_http} or {call_event}, if it has not already been
|
84
|
+
# run. However, you can call it explicitly to test its behavior. It cannot
|
85
|
+
# be called more than once for any given function.
|
86
|
+
#
|
87
|
+
# By default, the {FunctionsFramework.logger} will be used, but you can
|
88
|
+
# override that by providing your own logger. In particular, to disable
|
89
|
+
# logging, you can pass `Logger.new(nil)`.
|
90
|
+
#
|
91
|
+
# @param name [String] The name of the function to start up.
|
92
|
+
# @param logger [Logger] Use the given logger instead of the Functions
|
93
|
+
# Framework's global logger. Optional.
|
94
|
+
# @param lenient [Boolean] If false (the default), raise an error if the
|
95
|
+
# given function has already had its startup tasks run. If true,
|
96
|
+
# duplicate requests to run startup tasks are ignored.
|
97
|
+
# @return [Hash] The initialized globals.
|
98
|
+
#
|
99
|
+
def run_startup_tasks name, logger: nil, lenient: false
|
100
|
+
function = Testing.current_registry[name]
|
101
|
+
raise "Unknown function name #{name}" unless function
|
102
|
+
globals = Testing.current_globals name
|
103
|
+
if globals
|
104
|
+
raise "Function #{name} has already been started up" unless lenient
|
105
|
+
else
|
106
|
+
globals = function.populate_globals
|
107
|
+
Testing.current_registry.startup_tasks.each do |task|
|
108
|
+
task.call function, globals: globals, logger: logger
|
109
|
+
end
|
110
|
+
Testing.current_globals name, globals
|
111
|
+
end
|
112
|
+
globals.freeze
|
113
|
+
end
|
114
|
+
|
78
115
|
##
|
79
116
|
# Call the given HTTP function for testing. The underlying function must
|
80
|
-
# be of type `:http`.
|
117
|
+
# be of type `:http`. Returns the Rack response.
|
118
|
+
#
|
119
|
+
# By default, the startup tasks will be run for the given function if they
|
120
|
+
# have not already been run. You can, however, disable running startup
|
121
|
+
# tasks by providing an explicit globals hash.
|
122
|
+
#
|
123
|
+
# By default, the {FunctionsFramework.logger} will be used, but you can
|
124
|
+
# override that by providing your own logger. In particular, to disable
|
125
|
+
# logging, you can pass `Logger.new(nil)`.
|
81
126
|
#
|
82
127
|
# @param name [String] The name of the function to call
|
83
128
|
# @param request [Rack::Request] The Rack request to send
|
129
|
+
# @param globals [Hash] Do not run startup tasks, and instead provide the
|
130
|
+
# globals directly. Optional.
|
131
|
+
# @param logger [Logger] Use the given logger instead of the Functions
|
132
|
+
# Framework's global logger. Optional.
|
84
133
|
# @return [Rack::Response]
|
85
134
|
#
|
86
|
-
def call_http name, request
|
87
|
-
|
135
|
+
def call_http name, request, globals: nil, logger: nil
|
136
|
+
globals ||= run_startup_tasks name, logger: logger, lenient: true
|
137
|
+
function = Testing.current_registry[name]
|
88
138
|
case function&.type
|
89
139
|
when :http
|
90
|
-
Testing.interpret_response
|
140
|
+
Testing.interpret_response do
|
141
|
+
function.call request, globals: globals, logger: logger
|
142
|
+
end
|
91
143
|
when nil
|
92
144
|
raise "Unknown function name #{name}"
|
93
145
|
else
|
@@ -99,15 +151,28 @@ module FunctionsFramework
|
|
99
151
|
# Call the given event function for testing. The underlying function must
|
100
152
|
# be of type :cloud_event`.
|
101
153
|
#
|
154
|
+
# By default, the startup tasks will be run for the given function if they
|
155
|
+
# have not already been run. You can, however, disable running startup
|
156
|
+
# tasks by providing an explicit globals hash.
|
157
|
+
#
|
158
|
+
# By default, the {FunctionsFramework.logger} will be used, but you can
|
159
|
+
# override that by providing your own logger. In particular, to disable
|
160
|
+
# logging, you can pass `Logger.new(nil)`.
|
161
|
+
#
|
102
162
|
# @param name [String] The name of the function to call
|
103
163
|
# @param event [::CloudEvents::Event] The event to send
|
164
|
+
# @param globals [Hash] Do not run startup tasks, and instead provide the
|
165
|
+
# globals directly. Optional.
|
166
|
+
# @param logger [Logger] Use the given logger instead of the Functions
|
167
|
+
# Framework's global logger. Optional.
|
104
168
|
# @return [nil]
|
105
169
|
#
|
106
|
-
def call_event name, event
|
107
|
-
|
170
|
+
def call_event name, event, globals: nil, logger: nil
|
171
|
+
globals ||= run_startup_tasks name, logger: logger, lenient: true
|
172
|
+
function = Testing.current_registry[name]
|
108
173
|
case function&.type
|
109
174
|
when :cloud_event
|
110
|
-
function.
|
175
|
+
function.call event, globals: globals, logger: logger
|
111
176
|
nil
|
112
177
|
when nil
|
113
178
|
raise "Unknown function name #{name}"
|
@@ -208,27 +273,49 @@ module FunctionsFramework
|
|
208
273
|
extend self
|
209
274
|
|
210
275
|
@testing_registries = {}
|
276
|
+
@main_globals = {}
|
211
277
|
@mutex = ::Mutex.new
|
212
278
|
|
213
279
|
class << self
|
214
280
|
## @private
|
215
281
|
def load_for_testing path
|
216
282
|
old_registry = ::FunctionsFramework.global_registry
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
283
|
+
::Thread.current[:functions_framework_testing_registry] =
|
284
|
+
@mutex.synchronize do
|
285
|
+
if @testing_registries.key? path
|
286
|
+
::FunctionsFramework.global_registry = @testing_registries[path]
|
287
|
+
else
|
288
|
+
new_registry = ::FunctionsFramework::Registry.new
|
289
|
+
::FunctionsFramework.global_registry = new_registry
|
290
|
+
::Kernel.load path
|
291
|
+
@testing_registries[path] = new_registry
|
292
|
+
end
|
225
293
|
end
|
226
|
-
|
294
|
+
::Thread.current[:functions_framework_testing_globals] = {}
|
227
295
|
yield
|
228
296
|
ensure
|
297
|
+
::Thread.current[:functions_framework_testing_registry] = nil
|
298
|
+
::Thread.current[:functions_framework_testing_globals] = nil
|
229
299
|
::FunctionsFramework.global_registry = old_registry
|
230
300
|
end
|
231
301
|
|
302
|
+
## @private
|
303
|
+
def current_registry
|
304
|
+
::Thread.current[:functions_framework_testing_registry] ||
|
305
|
+
::FunctionsFramework.global_registry
|
306
|
+
end
|
307
|
+
|
308
|
+
## @private
|
309
|
+
def current_globals name, globals = nil
|
310
|
+
name = name.to_s
|
311
|
+
globals_by_name = ::Thread.current[:functions_framework_testing_globals] || @main_globals
|
312
|
+
if globals
|
313
|
+
globals_by_name[name] = globals
|
314
|
+
else
|
315
|
+
globals_by_name[name]
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
232
319
|
## @private
|
233
320
|
def interpret_response
|
234
321
|
response =
|
@@ -279,9 +366,10 @@ module FunctionsFramework
|
|
279
366
|
::Rack::RACK_ERRORS => ::StringIO.new
|
280
367
|
}
|
281
368
|
headers.each do |header|
|
282
|
-
|
369
|
+
case header
|
370
|
+
when String
|
283
371
|
name, value = header.split ":"
|
284
|
-
|
372
|
+
when ::Array
|
285
373
|
name, value = header
|
286
374
|
end
|
287
375
|
next unless name && value
|