functions_framework 0.7.0 → 0.11.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 +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 [](https://googlecloudplatform.github.io/functions-framework-ruby/latest) [](https://badge.fury.io/rb/functions_framework)
|
1
|
+
# Functions Framework for Ruby [](https://googlecloudplatform.github.io/functions-framework-ruby/latest) [](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
|