functions_framework 0.7.0 → 0.11.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 +26 -0
- data/README.md +3 -3
- data/docs/deploying-functions.md +7 -11
- data/docs/overview.md +2 -2
- data/docs/writing-functions.md +63 -15
- data/lib/functions_framework/cli.rb +1 -1
- data/lib/functions_framework/function.rb +53 -5
- data/lib/functions_framework/legacy_event_converter.rb +105 -26
- data/lib/functions_framework/server.rb +22 -11
- data/lib/functions_framework/testing.rb +14 -6
- data/lib/functions_framework/version.rb +1 -1
- metadata +25 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f9635b64ce67210f26441f5725d6ac6d46f470480411a93b736e470e108929a
|
4
|
+
data.tar.gz: 2c9df19eb02af7ecef29750bb0989eaaaa8ae6e6594fc5ebaf161d99fd6c477e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57c0cb8831225d0923cc31913693aa20f70fd245a7a5597ebbffe9d91aad51eeccd13a2c5697f010069afe529f143a320d29aa75c557edc6271f1361cacc9163
|
7
|
+
data.tar.gz: d8660d91dcdacbd5b35b44b3f8c9857d20bebb843d13c848651475e0f40884b564d6917e11b56394a3d7be0f711452308668ad66a6cdfbb05207b68a7ed23dc7
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
### v0.11.0 / 2021-06-28
|
4
|
+
|
5
|
+
* UPDATED: Update CloudEvents dependency to 0.5 to get fixes for JSON formatting cases
|
6
|
+
* FIXED: Updated Pub/Sub and Firebase event conversion logic to better align to Eventarc
|
7
|
+
|
8
|
+
### v0.10.0 / 2021-06-01
|
9
|
+
|
10
|
+
* ADDED: Support raw pubsub events sent by the pubsub emulator
|
11
|
+
* FIXED: Set proper response content-type charset when a function returns a string (plain text) or hash (JSON)
|
12
|
+
* FIXED: Properly handle conversion of non-ascii characters in legacy event strings
|
13
|
+
|
14
|
+
### v0.9.0 / 2021-03-18
|
15
|
+
|
16
|
+
* BREAKING CHANGE: Servers are configured as single-threaded in production by default, matching the current behavior of Google Cloud Functions.
|
17
|
+
* FIXED: Fixed conversion of Firebase events to CloudEvents to conform to the specs used by Cloud Functions and Cloud Run.
|
18
|
+
* FIXED: Fixed an error when reading a global set to a Minitest::Mock. This will make it easier to write tests that use mocks for global resources.
|
19
|
+
|
20
|
+
### v0.8.0 / 2021-03-02
|
21
|
+
|
22
|
+
* ADDED: Support for lazily-initialized globals
|
23
|
+
|
24
|
+
### v0.7.1 / 2021-01-26
|
25
|
+
|
26
|
+
* DOCS: Fixed several errors in the writing-functions doc samples
|
27
|
+
* DOCS: Updated documentation to note public release of GCF support
|
28
|
+
|
3
29
|
### v0.7.0 / 2020-09-25
|
4
30
|
|
5
31
|
* Now requires Ruby 2.5 or later.
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
# Functions Framework [![Documentation](https://img.shields.io/badge/docs-FunctionsFramework-red.svg)](https://googlecloudplatform.github.io/functions-framework-ruby/latest) [![Gem Version](https://badge.fury.io/rb/functions_framework.svg)](https://badge.fury.io/rb/functions_framework)
|
1
|
+
# Functions Framework for Ruby [![Documentation](https://img.shields.io/badge/docs-FunctionsFramework-red.svg)](https://googlecloudplatform.github.io/functions-framework-ruby/latest) [![Gem Version](https://badge.fury.io/rb/functions_framework.svg)](https://badge.fury.io/rb/functions_framework)
|
2
2
|
|
3
3
|
An open source framework for writing lightweight, portable Ruby functions that
|
4
4
|
run in a serverless environment. Functions written to this Framework will run
|
5
5
|
in many different environments, including:
|
6
6
|
|
7
|
-
* [Google Cloud Functions](https://cloud.google.com/functions) *(
|
7
|
+
* [Google Cloud Functions](https://cloud.google.com/functions) *(public preview)*
|
8
8
|
* [Google Cloud Run](https://cloud.google.com/run)
|
9
9
|
* Any other [Knative](https://github.com/knative)-based environment
|
10
10
|
* Your local development machine
|
@@ -60,7 +60,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
|
|
60
60
|
```ruby
|
61
61
|
# Gemfile
|
62
62
|
source "https://rubygems.org"
|
63
|
-
gem "functions_framework", "~> 0.
|
63
|
+
gem "functions_framework", "~> 0.11"
|
64
64
|
```
|
65
65
|
|
66
66
|
Create a file called `app.rb` and include the following code. This defines a
|
data/docs/deploying-functions.md
CHANGED
@@ -31,12 +31,8 @@ Functions Framework is designed especially for functions that can be hosted on
|
|
31
31
|
Cloud Functions.
|
32
32
|
|
33
33
|
You can run Ruby functions on Google Cloud Functions by selecting the `ruby26`
|
34
|
-
runtime
|
35
|
-
|
36
|
-
|
37
|
-
> **Note:** Ruby support on Cloud Functions is currently in limited alpha.
|
38
|
-
> It is not yet suitable for production workloads, and support is best-effort
|
39
|
-
> only. Access is currently limited to selected early-access users.
|
34
|
+
runtime or `ruby27` runtime to use a recent release of Ruby 2.6 or Ruby 2.7.
|
35
|
+
Support for Ruby 3.0 is forthcoming.
|
40
36
|
|
41
37
|
### Deploying and updating your function
|
42
38
|
|
@@ -64,7 +60,7 @@ Then, issue the gcloud command to deploy:
|
|
64
60
|
```sh
|
65
61
|
gcloud functions deploy $YOUR_FUNCTION_NAME \
|
66
62
|
--project=$YOUR_PROJECT_ID \
|
67
|
-
--runtime=
|
63
|
+
--runtime=ruby27 \
|
68
64
|
--trigger-http \
|
69
65
|
--entry-point=$YOUR_FUNCTION_TARGET
|
70
66
|
```
|
@@ -90,7 +86,7 @@ and above, set `FUNCTION_LOGGING_LEVEL` to `WARN` when deploying:
|
|
90
86
|
|
91
87
|
```sh
|
92
88
|
gcloud functions deploy $YOUR_FUNCTION_NAME --project=$YOUR_PROJECT_ID \
|
93
|
-
--runtime=
|
89
|
+
--runtime=ruby27 --trigger-http --source=$YOUR_FUNCTION_SOURCE \
|
94
90
|
--entry-point=$YOUR_FUNCTION_TARGET \
|
95
91
|
--set-env-vars=FUNCTION_LOGGING_LEVEL=WARN
|
96
92
|
```
|
@@ -125,7 +121,7 @@ Dockerfile that you can use as a starting point. Feel free to adjust it to the
|
|
125
121
|
needs of your project:
|
126
122
|
|
127
123
|
```
|
128
|
-
FROM ruby:2.
|
124
|
+
FROM ruby:2.7
|
129
125
|
WORKDIR /app
|
130
126
|
COPY . .
|
131
127
|
RUN gem install --no-document bundler \
|
@@ -151,8 +147,8 @@ command may ask you for permission to enable the Cloud Build API for the project
|
|
151
147
|
if it isn't already enabled.
|
152
148
|
|
153
149
|
Because you provide your own Docker image when deploying to Cloud Run, you can
|
154
|
-
use any version of Ruby supported by the Functions Framework, from 2.
|
155
|
-
|
150
|
+
use any version of Ruby supported by the Functions Framework, from 2.5 through
|
151
|
+
3.0.
|
156
152
|
|
157
153
|
### Deploying an image to Cloud Run
|
158
154
|
|
data/docs/overview.md
CHANGED
@@ -8,7 +8,7 @@ The Functions Framework is an open source framework for writing lightweight,
|
|
8
8
|
portable Ruby functions that run in a serverless environment. Functions written
|
9
9
|
to this Framework will run in many different environments, including:
|
10
10
|
|
11
|
-
* [Google Cloud Functions](https://cloud.google.com/functions) *(
|
11
|
+
* [Google Cloud Functions](https://cloud.google.com/functions) *(public preview)*
|
12
12
|
* [Google Cloud Run](https://cloud.google.com/run)
|
13
13
|
* Any other [Knative](https://github.com/knative)-based environment
|
14
14
|
* Your local development machine
|
@@ -64,7 +64,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
|
|
64
64
|
```ruby
|
65
65
|
# Gemfile
|
66
66
|
source "https://rubygems.org"
|
67
|
-
gem "functions_framework", "~> 0.
|
67
|
+
gem "functions_framework", "~> 0.11"
|
68
68
|
```
|
69
69
|
|
70
70
|
Create a file called `app.rb` and include the following code. This defines a
|
data/docs/writing-functions.md
CHANGED
@@ -53,7 +53,7 @@ require "functions_framework"
|
|
53
53
|
|
54
54
|
FunctionsFramework.http "request_info_example" do |request|
|
55
55
|
# Include some request info in the response body.
|
56
|
-
"Received #{request.
|
56
|
+
"Received #{request.request_method} from #{request.url}!\n"
|
57
57
|
end
|
58
58
|
```
|
59
59
|
|
@@ -68,7 +68,7 @@ require "functions_framework"
|
|
68
68
|
|
69
69
|
FunctionsFramework.http "logging_example" do |request|
|
70
70
|
# Log some request info.
|
71
|
-
request.logger.info "I received #{request.
|
71
|
+
request.logger.info "I received #{request.request_method} from #{request.url}!"
|
72
72
|
# A simple response body.
|
73
73
|
"ok"
|
74
74
|
end
|
@@ -111,7 +111,7 @@ dependency on Sinatra in your `Gemfile`:
|
|
111
111
|
|
112
112
|
```ruby
|
113
113
|
source "https://rubygems.org"
|
114
|
-
gem "functions_framework", "~> 0.
|
114
|
+
gem "functions_framework", "~> 0.11"
|
115
115
|
gem "sinatra", "~> 2.0"
|
116
116
|
```
|
117
117
|
|
@@ -248,10 +248,10 @@ FunctionsFramework.http "hello" do |request|
|
|
248
248
|
end
|
249
249
|
```
|
250
250
|
|
251
|
-
Startup tasks are run once per Ruby instance
|
252
|
-
|
253
|
-
|
254
|
-
function is executed.
|
251
|
+
Startup tasks are run once per Ruby instance during cold start -- that is,
|
252
|
+
after the Ruby VM boots up but before the framework starts receiving requests
|
253
|
+
and executing functions. You can define multiple startup tasks, and they will
|
254
|
+
run in order, and are guaranteed to complete before any function is executed.
|
255
255
|
|
256
256
|
The block is optionally passed the {FunctionsFramework::Function} representing
|
257
257
|
the function that will be run. You code can, for example, perform different
|
@@ -286,6 +286,11 @@ end
|
|
286
286
|
# ...
|
287
287
|
```
|
288
288
|
|
289
|
+
Because startup tasks run during cold start, they could have an impact on your
|
290
|
+
function's startup latency. To mitigate this issue, it is possible to run parts
|
291
|
+
of your initialization lazily, as described below in the section below on
|
292
|
+
[lazy initialization](#Lazy_initialization).
|
293
|
+
|
289
294
|
### The execution context and global data
|
290
295
|
|
291
296
|
When your function block executes, the _object context_ (i.e. `self`) is set to
|
@@ -331,8 +336,9 @@ resources, as described below.
|
|
331
336
|
Using the global data mechanism is generally preferred over actual Ruby global
|
332
337
|
variables, because the Functions Framework can help you avoid concurrent edits.
|
333
338
|
Additionally, the framework will isolate the sets of global data associated
|
334
|
-
with different sets of functions, which lets you
|
335
|
-
|
339
|
+
with different sets of functions, which lets you run functions in isolation
|
340
|
+
during unit tests. If you are testing multiple functions, they will not
|
341
|
+
interfere with each other as they might if they used global variables.
|
336
342
|
|
337
343
|
### Sharing resources
|
338
344
|
|
@@ -345,10 +351,10 @@ re-establishing it for every function invocation.
|
|
345
351
|
|
346
352
|
The best practice for sharing a resource across function invocations is to
|
347
353
|
initialize it in a {FunctionsFramework.on_startup} block, and reference it from
|
348
|
-
global shared data. (As discussed above,
|
349
|
-
in a startup task rather than at the top level of a Ruby file,
|
350
|
-
the Functions Framework's global data mechanism rather than Ruby's
|
351
|
-
variables.)
|
354
|
+
global shared data. (As discussed above, the best practice is to initialize
|
355
|
+
shared resources in a startup task rather than at the top level of a Ruby file,
|
356
|
+
and to use the Functions Framework's global data mechanism rather than Ruby's
|
357
|
+
global variables.)
|
352
358
|
|
353
359
|
Here is a simple example:
|
354
360
|
|
@@ -383,6 +389,48 @@ may perform CPU throttling, and therefore there may not be an opportunity for
|
|
383
389
|
cleanup tasks to run. (For example, you could register a `Kernel.at_exit` task,
|
384
390
|
but the Ruby VM may still terminate without calling it.)
|
385
391
|
|
392
|
+
### Lazy initialization
|
393
|
+
|
394
|
+
Because startup tasks run during cold start, they could have an impact on your
|
395
|
+
function's startup latency. You can mitigate this by initializing some globals
|
396
|
+
_lazily_. When setting a global, instead of computing and setting the value
|
397
|
+
directly (e.g. constructing a shared API client object directly), you can
|
398
|
+
provide a block that describes how to construct it on demand.
|
399
|
+
|
400
|
+
Here is an example using the storage client we saw above.
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
require "functions_framework"
|
404
|
+
|
405
|
+
# This startup block describes _how_ to initialize a shared client, but
|
406
|
+
# does not construct it immediately.
|
407
|
+
FunctionsFramework.on_startup do
|
408
|
+
require "google/cloud/storage"
|
409
|
+
set_global :storage_client do
|
410
|
+
Google::Cloud::Storage.new
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# The first time this function is invoked, it will call the above block
|
415
|
+
# to construct the storage client. Subsequent invocations will not need
|
416
|
+
# to construct it again, but will reuse the same shared object.
|
417
|
+
FunctionsFramework.http "storage_example" do |request|
|
418
|
+
bucket = global(:storage_client).bucket "my-bucket"
|
419
|
+
file = bucket.file "path/to/my-file.txt"
|
420
|
+
file.download.to_s
|
421
|
+
end
|
422
|
+
```
|
423
|
+
|
424
|
+
The block will not be called until a function actually attempts to access the
|
425
|
+
global. From that point, subsequent accesses of the global will return that
|
426
|
+
same shared value; the block will be called at most once. This is true even if
|
427
|
+
multiple functions are run concurrently in different threads.
|
428
|
+
|
429
|
+
Lazy initialization is particularly useful if you define several different
|
430
|
+
functions that may use different sets of shared resources. Instead of
|
431
|
+
initializing all resources eagerly up front, you could initialize them lazily
|
432
|
+
and run only the code needed by the function that is actually invoked.
|
433
|
+
|
386
434
|
## Structuring a project
|
387
435
|
|
388
436
|
A Functions Framework based "project" or "application" is a typical Ruby
|
@@ -422,7 +470,7 @@ Following is a typical layout for a Functions Framework based project.
|
|
422
470
|
```ruby
|
423
471
|
# Gemfile
|
424
472
|
source "https://rubygems.org"
|
425
|
-
gem "functions_framework", "~> 0.
|
473
|
+
gem "functions_framework", "~> 0.11"
|
426
474
|
```
|
427
475
|
|
428
476
|
```ruby
|
@@ -446,7 +494,7 @@ class Hello
|
|
446
494
|
end
|
447
495
|
|
448
496
|
def build_response
|
449
|
-
"Received request: #{request.
|
497
|
+
"Received request: #{@request.request_method} #{@request.url}\n"
|
450
498
|
end
|
451
499
|
end
|
452
500
|
```
|
@@ -74,7 +74,7 @@ module FunctionsFramework
|
|
74
74
|
# @param argv [Array<String>]
|
75
75
|
# @return [self]
|
76
76
|
#
|
77
|
-
def parse_args argv # rubocop:disable Metrics/MethodLength
|
77
|
+
def parse_args argv # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
78
78
|
@option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
|
79
79
|
op.on "-t", "--target TARGET",
|
80
80
|
"Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
|
@@ -164,6 +164,28 @@ module FunctionsFramework
|
|
164
164
|
callable.call(*args)
|
165
165
|
end
|
166
166
|
|
167
|
+
##
|
168
|
+
# A lazy evaluator for a global
|
169
|
+
# @private
|
170
|
+
#
|
171
|
+
class LazyGlobal
|
172
|
+
def initialize block
|
173
|
+
@block = block
|
174
|
+
@value = nil
|
175
|
+
@mutex = ::Mutex.new
|
176
|
+
end
|
177
|
+
|
178
|
+
def value
|
179
|
+
@mutex.synchronize do
|
180
|
+
if @block
|
181
|
+
@value = @block.call
|
182
|
+
@block = nil
|
183
|
+
end
|
184
|
+
@value
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
167
189
|
##
|
168
190
|
# A base class for a callable object that provides calling context.
|
169
191
|
#
|
@@ -196,7 +218,9 @@ module FunctionsFramework
|
|
196
218
|
# @return [Object]
|
197
219
|
#
|
198
220
|
def global key
|
199
|
-
@__globals[key]
|
221
|
+
value = @__globals[key]
|
222
|
+
value = value.value if LazyGlobal === value
|
223
|
+
value
|
200
224
|
end
|
201
225
|
|
202
226
|
##
|
@@ -204,11 +228,35 @@ module FunctionsFramework
|
|
204
228
|
# are frozen when the server starts, so this call will raise an exception
|
205
229
|
# if called from a normal function.
|
206
230
|
#
|
207
|
-
#
|
208
|
-
#
|
231
|
+
# You can set a global to a final value, or you can provide a block that
|
232
|
+
# lazily computes the global the first time it is requested.
|
233
|
+
#
|
234
|
+
# @overload set_global(key, value)
|
235
|
+
# Set the given global to the given value. For example:
|
236
|
+
#
|
237
|
+
# set_global(:project_id, "my-project-id")
|
238
|
+
#
|
239
|
+
# @param key [Symbol,String]
|
240
|
+
# @param value [Object]
|
241
|
+
# @return [self]
|
242
|
+
#
|
243
|
+
# @overload set_global(key, &block)
|
244
|
+
# Call the given block to compute the global's value only when the
|
245
|
+
# value is actually requested. This block will be called at most once,
|
246
|
+
# and its result reused for subsequent calls. For example:
|
247
|
+
#
|
248
|
+
# set_global(:connection_pool) do
|
249
|
+
# ExpensiveConnectionPool.new
|
250
|
+
# end
|
251
|
+
#
|
252
|
+
# @param key [Symbol,String]
|
253
|
+
# @param block [Proc] A block that lazily computes a value
|
254
|
+
# @yieldreturn [Object] The value
|
255
|
+
# @return [self]
|
209
256
|
#
|
210
|
-
def set_global key, value
|
211
|
-
@__globals[key] = value
|
257
|
+
def set_global key, value = nil, &block
|
258
|
+
@__globals[key] = block ? LazyGlobal.new(block) : value
|
259
|
+
self
|
212
260
|
end
|
213
261
|
|
214
262
|
##
|
@@ -27,20 +27,21 @@ module FunctionsFramework
|
|
27
27
|
# @return [nil] if the event format was not recognized.
|
28
28
|
#
|
29
29
|
def decode_rack_env env
|
30
|
-
content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"]
|
30
|
+
content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"], default_charset: "utf-8"
|
31
31
|
return nil unless content_type.media_type == "application" && content_type.subtype_base == "json"
|
32
32
|
input = read_input_json env["rack.input"], content_type.charset
|
33
33
|
return nil unless input
|
34
|
+
input = convert_raw_pubsub_event input, env if raw_pubsub_payload? input
|
34
35
|
context = normalized_context input
|
35
36
|
return nil unless context
|
36
|
-
construct_cloud_event context, input["data"]
|
37
|
+
construct_cloud_event context, input["data"]
|
37
38
|
end
|
38
39
|
|
39
40
|
private
|
40
41
|
|
41
42
|
def read_input_json input, charset
|
42
43
|
input = input.read if input.respond_to? :read
|
43
|
-
input
|
44
|
+
input.force_encoding charset if charset
|
44
45
|
content = ::JSON.parse input
|
45
46
|
content = nil unless content.is_a? ::Hash
|
46
47
|
content
|
@@ -48,15 +49,50 @@ module FunctionsFramework
|
|
48
49
|
nil
|
49
50
|
end
|
50
51
|
|
52
|
+
def raw_pubsub_payload? input
|
53
|
+
return false if input.include?("context") || !input.include?("subscription")
|
54
|
+
message = input["message"]
|
55
|
+
message.is_a?(::Hash) && message.include?("data") && message.include?("messageId")
|
56
|
+
end
|
57
|
+
|
58
|
+
def convert_raw_pubsub_event input, env
|
59
|
+
message = input["message"]
|
60
|
+
path = "#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
|
61
|
+
path_match = %r{projects/[^/?]+/topics/[^/?]+}.match path
|
62
|
+
topic = path_match ? path_match[0] : "UNKNOWN_PUBSUB_TOPIC"
|
63
|
+
timestamp = message["publishTime"] || ::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
|
64
|
+
{
|
65
|
+
"context" => {
|
66
|
+
"eventId" => message["messageId"],
|
67
|
+
"timestamp" => timestamp,
|
68
|
+
"eventType" => "google.pubsub.topic.publish",
|
69
|
+
"resource" => {
|
70
|
+
"service" => "pubsub.googleapis.com",
|
71
|
+
"type" => "type.googleapis.com/google.pubsub.v1.PubsubMessage",
|
72
|
+
"name" => topic
|
73
|
+
}
|
74
|
+
},
|
75
|
+
"data" => {
|
76
|
+
"@type" => "type.googleapis.com/google.pubsub.v1.PubsubMessage",
|
77
|
+
"data" => message["data"],
|
78
|
+
"attributes" => message["attributes"]
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
51
83
|
def normalized_context input
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
service, resource = analyze_resource
|
84
|
+
id = normalized_context_field input, "eventId"
|
85
|
+
timestamp = normalized_context_field input, "timestamp"
|
86
|
+
type = normalized_context_field input, "eventType"
|
87
|
+
domain = normalized_context_field input, "domain"
|
88
|
+
service, resource = analyze_resource normalized_context_field input, "resource"
|
57
89
|
service ||= service_from_type type
|
58
90
|
return nil unless id && timestamp && type && service && resource
|
59
|
-
{ id: id, timestamp: timestamp, type: type, service: service, resource: resource }
|
91
|
+
{ id: id, timestamp: timestamp, type: type, service: service, resource: resource, domain: domain }
|
92
|
+
end
|
93
|
+
|
94
|
+
def normalized_context_field input, field
|
95
|
+
input["context"]&.[](field) || input[field]
|
60
96
|
end
|
61
97
|
|
62
98
|
def analyze_resource raw_resource
|
@@ -78,37 +114,66 @@ module FunctionsFramework
|
|
78
114
|
nil
|
79
115
|
end
|
80
116
|
|
81
|
-
def construct_cloud_event context, data
|
82
|
-
source, subject = convert_source context[:service], context[:resource]
|
117
|
+
def construct_cloud_event context, data
|
118
|
+
source, subject = convert_source context[:service], context[:resource], context[:domain]
|
83
119
|
type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
|
84
120
|
return nil unless type && source
|
85
|
-
ce_data = convert_data context
|
86
|
-
content_type = "application/json
|
121
|
+
ce_data, data_subject = convert_data context, data
|
122
|
+
content_type = "application/json"
|
87
123
|
::CloudEvents::Event.new id: context[:id],
|
88
124
|
source: source,
|
89
125
|
type: type,
|
90
126
|
spec_version: "1.0",
|
91
127
|
data_content_type: content_type,
|
92
128
|
data: ce_data,
|
93
|
-
subject: subject,
|
129
|
+
subject: subject || data_subject,
|
94
130
|
time: context[:timestamp]
|
95
131
|
end
|
96
132
|
|
97
|
-
def convert_source service, resource
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
133
|
+
def convert_source service, resource, domain
|
134
|
+
return ["//#{service}/#{resource}", nil] unless CE_SERVICE_TO_RESOURCE_RE.key? service
|
135
|
+
|
136
|
+
match = CE_SERVICE_TO_RESOURCE_RE[service].match resource
|
137
|
+
return [nil, nil] unless match
|
138
|
+
resource_fragment = match[1]
|
139
|
+
subject = match[2]
|
140
|
+
|
141
|
+
if service == "firebasedatabase.googleapis.com"
|
142
|
+
location =
|
143
|
+
case domain
|
144
|
+
when "firebaseio.com"
|
145
|
+
"us-central1"
|
146
|
+
when /^([\w-]+)\./
|
147
|
+
Regexp.last_match[1]
|
148
|
+
else
|
149
|
+
return [nil, nil]
|
150
|
+
end
|
151
|
+
["//#{service}/projects/_/locations/#{location}/#{resource_fragment}", subject]
|
102
152
|
else
|
103
|
-
["//#{service}/#{
|
153
|
+
["//#{service}/#{resource_fragment}", subject]
|
104
154
|
end
|
105
155
|
end
|
106
156
|
|
107
|
-
def convert_data
|
108
|
-
|
109
|
-
|
157
|
+
def convert_data context, data
|
158
|
+
service = context[:service]
|
159
|
+
case service
|
160
|
+
when "pubsub.googleapis.com"
|
161
|
+
data["messageId"] = context[:id]
|
162
|
+
data["publishTime"] = context[:timestamp]
|
163
|
+
[{ "message" => data }, nil]
|
164
|
+
when "firebaseauth.googleapis.com"
|
165
|
+
if data.key? "metadata"
|
166
|
+
FIREBASE_AUTH_METADATA_LEGACY_TO_CE.each do |old_key, new_key|
|
167
|
+
if data["metadata"].key? old_key
|
168
|
+
data["metadata"][new_key] = data["metadata"][old_key]
|
169
|
+
data["metadata"].delete old_key
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
subject = "users/#{data['uid']}" if data.key? "uid"
|
174
|
+
[data, subject]
|
110
175
|
else
|
111
|
-
data
|
176
|
+
[data, nil]
|
112
177
|
end
|
113
178
|
end
|
114
179
|
|
@@ -116,8 +181,9 @@ module FunctionsFramework
|
|
116
181
|
%r{^providers/cloud\.firestore/} => "firestore.googleapis.com",
|
117
182
|
%r{^providers/cloud\.pubsub/} => "pubsub.googleapis.com",
|
118
183
|
%r{^providers/cloud\.storage/} => "storage.googleapis.com",
|
119
|
-
%r{^providers/firebase\.auth/} => "
|
120
|
-
%r{^providers/google\.firebase}
|
184
|
+
%r{^providers/firebase\.auth/} => "firebaseauth.googleapis.com",
|
185
|
+
%r{^providers/google\.firebase\.analytics/} => "firebase.googleapis.com",
|
186
|
+
%r{^providers/google\.firebase\.database/} => "firebasedatabase.googleapis.com"
|
121
187
|
}.freeze
|
122
188
|
|
123
189
|
LEGACY_TYPE_TO_CE_TYPE = {
|
@@ -140,5 +206,18 @@ module FunctionsFramework
|
|
140
206
|
"providers/google.firebase.database/eventTypes/ref.delete" => "google.firebase.database.document.v1.deleted",
|
141
207
|
"providers/cloud.storage/eventTypes/object.change" => "google.cloud.storage.object.v1.finalized"
|
142
208
|
}.freeze
|
209
|
+
|
210
|
+
CE_SERVICE_TO_RESOURCE_RE = {
|
211
|
+
"firebase.googleapis.com" => %r{^(projects/[^/]+)/(events/[^/]+)$},
|
212
|
+
"firebasedatabase.googleapis.com" => %r{^projects/_/(instances/[^/]+)/(refs/.+)$},
|
213
|
+
"firestore.googleapis.com" => %r{^(projects/[^/]+/databases/\(default\))/(documents/.+)$},
|
214
|
+
"storage.googleapis.com" => %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}
|
215
|
+
}.freeze
|
216
|
+
|
217
|
+
# Map Firebase Auth legacy event metadata field names to their equivalent CloudEvent field names.
|
218
|
+
FIREBASE_AUTH_METADATA_LEGACY_TO_CE = {
|
219
|
+
"createdAt" => "createTime",
|
220
|
+
"lastSignedInAt" => "lastSignInTime"
|
221
|
+
}.freeze
|
143
222
|
end
|
144
223
|
end
|
@@ -158,7 +158,7 @@ module FunctionsFramework
|
|
158
158
|
::Signal.trap "SIGHUP" do
|
159
159
|
Server.signal_enqueue "SIGHUP", @config.logger, @server
|
160
160
|
end
|
161
|
-
rescue ::ArgumentError
|
161
|
+
rescue ::ArgumentError
|
162
162
|
# Not available on all systems
|
163
163
|
end
|
164
164
|
@signals_installed = true
|
@@ -306,7 +306,7 @@ module FunctionsFramework
|
|
306
306
|
# @return [Integer]
|
307
307
|
#
|
308
308
|
def max_threads
|
309
|
-
@max_threads ||
|
309
|
+
@max_threads || 1
|
310
310
|
end
|
311
311
|
|
312
312
|
##
|
@@ -346,9 +346,9 @@ module FunctionsFramework
|
|
346
346
|
when ::Rack::Response
|
347
347
|
response.finish
|
348
348
|
when ::String
|
349
|
-
string_response response,
|
349
|
+
string_response response, 200
|
350
350
|
when ::Hash
|
351
|
-
string_response ::JSON.dump(response), "application/json"
|
351
|
+
string_response ::JSON.dump(response), 200, content_type: "application/json"
|
352
352
|
when ::CloudEvents::CloudEventsError
|
353
353
|
cloud_events_error_response response
|
354
354
|
when ::StandardError
|
@@ -359,10 +359,17 @@ module FunctionsFramework
|
|
359
359
|
end
|
360
360
|
|
361
361
|
def notfound_response
|
362
|
-
string_response "Not found",
|
362
|
+
string_response "Not found", 404
|
363
363
|
end
|
364
364
|
|
365
|
-
def string_response string,
|
365
|
+
def string_response string, status, content_type: nil
|
366
|
+
string.force_encoding ::Encoding::ASCII_8BIT unless string.valid_encoding?
|
367
|
+
if string.encoding == ::Encoding::ASCII_8BIT
|
368
|
+
content_type ||= "application/octet-stream"
|
369
|
+
else
|
370
|
+
content_type ||= "text/plain"
|
371
|
+
content_type = "#{content_type}; charset=#{string.encoding.name.downcase}"
|
372
|
+
end
|
366
373
|
headers = {
|
367
374
|
"Content-Type" => content_type,
|
368
375
|
"Content-Length" => string.bytesize
|
@@ -372,13 +379,13 @@ module FunctionsFramework
|
|
372
379
|
|
373
380
|
def cloud_events_error_response error
|
374
381
|
@config.logger.warn error
|
375
|
-
string_response "#{error.class}: #{error.message}",
|
382
|
+
string_response "#{error.class}: #{error.message}", 400
|
376
383
|
end
|
377
384
|
|
378
385
|
def error_response message
|
379
386
|
@config.logger.error message
|
380
387
|
message = "Unexpected internal error" unless @config.show_error_details?
|
381
|
-
string_response message,
|
388
|
+
string_response message, 500
|
382
389
|
end
|
383
390
|
end
|
384
391
|
|
@@ -424,7 +431,7 @@ module FunctionsFramework
|
|
424
431
|
when ::CloudEvents::Event
|
425
432
|
handle_cloud_event event, logger
|
426
433
|
when ::Array
|
427
|
-
::CloudEvents::
|
434
|
+
::CloudEvents::CloudEventsError.new "Batched CloudEvents are not supported"
|
428
435
|
when ::CloudEvents::CloudEventsError
|
429
436
|
event
|
430
437
|
else
|
@@ -436,9 +443,13 @@ module FunctionsFramework
|
|
436
443
|
private
|
437
444
|
|
438
445
|
def decode_event env
|
439
|
-
|
446
|
+
begin
|
447
|
+
@cloud_events.decode_event env
|
448
|
+
rescue ::CloudEvents::NotCloudEventError
|
449
|
+
env["rack.input"].rewind rescue nil
|
440
450
|
@legacy_events.decode_rack_env(env) ||
|
441
|
-
|
451
|
+
raise(::CloudEvents::CloudEventsError, "Unrecognized event format")
|
452
|
+
end
|
442
453
|
rescue ::CloudEvents::CloudEventsError => e
|
443
454
|
e
|
444
455
|
end
|
@@ -330,20 +330,27 @@ module FunctionsFramework
|
|
330
330
|
when ::Array
|
331
331
|
::Rack::Response.new response[2], response[0], response[1]
|
332
332
|
when ::String
|
333
|
-
string_response response,
|
333
|
+
string_response response, 200
|
334
334
|
when ::Hash
|
335
335
|
json = ::JSON.dump response
|
336
|
-
string_response json, "application/json"
|
336
|
+
string_response json, 200, content_type: "application/json"
|
337
337
|
when ::StandardError
|
338
338
|
message = "#{response.class}: #{response.message}\n#{response.backtrace}\n"
|
339
|
-
string_response message,
|
339
|
+
string_response message, 500
|
340
340
|
else
|
341
341
|
raise "Unexpected response type: #{response.inspect}"
|
342
342
|
end
|
343
343
|
end
|
344
344
|
|
345
345
|
## @private
|
346
|
-
def string_response string,
|
346
|
+
def string_response string, status, content_type: nil
|
347
|
+
string.force_encoding ::Encoding::ASCII_8BIT unless string.valid_encoding?
|
348
|
+
if string.encoding == ::Encoding::ASCII_8BIT
|
349
|
+
content_type ||= "application/octet-stream"
|
350
|
+
else
|
351
|
+
content_type ||= "text/plain"
|
352
|
+
content_type = "#{content_type}; charset=#{string.encoding.name.downcase}"
|
353
|
+
end
|
347
354
|
headers = {
|
348
355
|
"Content-Type" => content_type,
|
349
356
|
"Content-Length" => string.bytesize
|
@@ -366,9 +373,10 @@ module FunctionsFramework
|
|
366
373
|
::Rack::RACK_ERRORS => ::StringIO.new
|
367
374
|
}
|
368
375
|
headers.each do |header|
|
369
|
-
|
376
|
+
case header
|
377
|
+
when String
|
370
378
|
name, value = header.split ":"
|
371
|
-
|
379
|
+
when ::Array
|
372
380
|
name, value = header
|
373
381
|
end
|
374
382
|
next unless name && value
|
metadata
CHANGED
@@ -1,43 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: functions_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Azuma
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cloud_events
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.5.1
|
20
|
+
- - "<"
|
18
21
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
22
|
+
version: 2.a
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.5.1
|
30
|
+
- - "<"
|
25
31
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
32
|
+
version: 2.a
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: puma
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
30
36
|
requirements:
|
31
|
-
- - "
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 4.3.0
|
40
|
+
- - "<"
|
32
41
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
42
|
+
version: 6.a
|
34
43
|
type: :runtime
|
35
44
|
prerelease: false
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
37
46
|
requirements:
|
38
|
-
- - "
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 4.3.0
|
50
|
+
- - "<"
|
39
51
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
52
|
+
version: 6.a
|
41
53
|
- !ruby/object:Gem::Dependency
|
42
54
|
name: rack
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,10 +99,10 @@ homepage: https://github.com/GoogleCloudPlatform/functions-framework-ruby
|
|
87
99
|
licenses:
|
88
100
|
- Apache-2.0
|
89
101
|
metadata:
|
90
|
-
changelog_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.
|
102
|
+
changelog_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.11.0/file.CHANGELOG.html
|
91
103
|
source_code_uri: https://github.com/GoogleCloudPlatform/functions-framework-ruby
|
92
104
|
bug_tracker_uri: https://github.com/GoogleCloudPlatform/functions-framework-ruby/issues
|
93
|
-
documentation_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.
|
105
|
+
documentation_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.11.0
|
94
106
|
post_install_message:
|
95
107
|
rdoc_options: []
|
96
108
|
require_paths:
|
@@ -106,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
118
|
- !ruby/object:Gem::Version
|
107
119
|
version: '0'
|
108
120
|
requirements: []
|
109
|
-
rubygems_version: 3.
|
121
|
+
rubygems_version: 3.1.6
|
110
122
|
signing_key:
|
111
123
|
specification_version: 4
|
112
124
|
summary: Functions Framework for Ruby
|