functions_framework 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,122 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "base64"
16
+ require "json"
17
+
18
+ module FunctionsFramework
19
+ module CloudEvents
20
+ ##
21
+ # An implementation of JSON format and JSON batch format.
22
+ #
23
+ # See https://github.com/cloudevents/spec/blob/master/json-format.md
24
+ #
25
+ class JsonFormat
26
+ ##
27
+ # Decode an event from the given input JSON string.
28
+ #
29
+ # @param json [String] A JSON-formatted string
30
+ # @return [FunctionsFramework::CloudEvents::Event]
31
+ #
32
+ def decode json, **_other_kwargs
33
+ structure = ::JSON.parse json
34
+ decode_hash_structure structure
35
+ end
36
+
37
+ ##
38
+ # Encode an event to a JSON string.
39
+ #
40
+ # @param event [FunctionsFramework::CloudEvents::Event] An input event.
41
+ # @param sort [boolean] Whether to sort keys of the JSON output.
42
+ # @return [String] The JSON representation.
43
+ #
44
+ def encode event, sort: false, **_other_kwargs
45
+ structure = encode_hash_structure event
46
+ structure = sort_keys structure if sort
47
+ ::JSON.dump structure
48
+ end
49
+
50
+ ##
51
+ # Decode a batch of events from the given input string.
52
+ #
53
+ # @param json [String] A JSON-formatted string
54
+ # @return [Array<FunctionsFramework::CloudEvents::Event>]
55
+ #
56
+ def decode_batch json, **_other_kwargs
57
+ structure_array = Array(::JSON.parse(json))
58
+ structure_array.map do |structure|
59
+ decode_hash_structure structure
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Encode a batch of event to a JSON string.
65
+ #
66
+ # @param events [Array<FunctionsFramework::CloudEvents::Event>] An array
67
+ # of input events.
68
+ # @param sort [boolean] Whether to sort keys of the JSON output.
69
+ # @return [String] The JSON representation.
70
+ #
71
+ def encode_batch events, sort: false, **_other_kwargs
72
+ structure_array = Array(events).map do |event|
73
+ structure = encode_hash_structure event
74
+ sort ? sort_keys(structure) : structure
75
+ end
76
+ ::JSON.dump structure_array
77
+ end
78
+
79
+ ##
80
+ # Decode a single event from a hash data structure with keys and types
81
+ # conforming to the JSON event format.
82
+ #
83
+ # @param structure [Hash] An input hash.
84
+ # @return [FunctionsFramework::CloudEvents::Event]
85
+ #
86
+ def decode_hash_structure structure
87
+ if structure.key? "data_base64"
88
+ structure = structure.dup
89
+ structure["data"] = ::Base64.decode64 structure.delete "data_base64"
90
+ end
91
+ Event.create spec_version: structure["specversion"], attributes: structure
92
+ end
93
+
94
+ ##
95
+ # Encode a single event to a hash data structure with keys and types
96
+ # conforming to the JSON event format.
97
+ #
98
+ # @param event [FunctionsFramework::CloudEvents::Event] An input event.
99
+ # @return [String] The hash structure.
100
+ #
101
+ def encode_hash_structure event
102
+ structure = event.to_h
103
+ data = structure["data"]
104
+ if data.is_a?(::String) && data.encoding == ::Encoding::ASCII_8BIT
105
+ structure.delete "data"
106
+ structure["data_base64"] = ::Base64.encode64 data
107
+ end
108
+ structure
109
+ end
110
+
111
+ private
112
+
113
+ def sort_keys hash
114
+ result = {}
115
+ hash.keys.sort.each do |key|
116
+ result[key] = hash[key]
117
+ end
118
+ result
119
+ end
120
+ end
121
+ end
122
+ end
@@ -54,11 +54,9 @@ module FunctionsFramework
54
54
  #
55
55
  # * A `:http` type function takes a `Rack::Request` argument, and returns
56
56
  # a Rack response type. See {FunctionsFramework::Registry.add_http}.
57
- # * A `:event` or `:cloud_event` type function takes a
57
+ # * A `:cloud_event` type function takes a
58
58
  # {FunctionsFramework::CloudEvents::Event} argument, and does not
59
59
  # return a value. See {FunctionsFramework::Registry.add_cloud_event}.
60
- # Note that for an `:event` type function, the passed event argument is
61
- # split into two arguments when passed to the underlying block.
62
60
  #
63
61
  # @param argument [Rack::Request,FunctionsFramework::CloudEvents::Event]
64
62
  # @return [Object]
@@ -0,0 +1,136 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "json"
16
+
17
+ module FunctionsFramework
18
+ ##
19
+ # Converter from legacy GCF event formats to CloudEvents.
20
+ #
21
+ class LegacyEventConverter
22
+ ##
23
+ # Decode an event from the given Rack environment hash.
24
+ #
25
+ # @param env [Hash] The Rack environment
26
+ # @return [FunctionsFramework::CloudEvents::Event] if the request could
27
+ # be converted
28
+ # @return [nil] if the event format was not recognized.
29
+ #
30
+ def decode_rack_env env
31
+ content_type = CloudEvents::ContentType.new env["CONTENT_TYPE"]
32
+ return nil unless content_type.media_type == "application" && content_type.subtype_prefix == "json"
33
+ input = read_input_json env["rack.input"], content_type.charset
34
+ return nil unless input
35
+ raw_context = input["context"] || input
36
+ context = normalized_context raw_context
37
+ return nil unless context
38
+ construct_cloud_event context, input["data"]
39
+ end
40
+
41
+ private
42
+
43
+ def read_input_json input, charset
44
+ input = input.read if input.respond_to? :read
45
+ input = input.encode charset if charset
46
+ content = ::JSON.parse input
47
+ content = nil unless content.is_a? ::Hash
48
+ content
49
+ rescue ::JSON::ParserError
50
+ nil
51
+ end
52
+
53
+ def normalized_context raw_context
54
+ id = raw_context["eventId"]
55
+ return nil unless id
56
+ timestamp = raw_context["timestamp"]
57
+ return nil unless timestamp
58
+ type = raw_context["eventType"]
59
+ return nil unless type
60
+ service, resource = analyze_resource raw_context["resource"], type
61
+ return nil unless service && resource
62
+ { id: id, timestamp: timestamp, type: type, service: service, resource: resource }
63
+ end
64
+
65
+ def analyze_resource raw_resource, type
66
+ case raw_resource
67
+ when ::Hash
68
+ [raw_resource["service"], raw_resource["name"]]
69
+ when ::String
70
+ [service_from_type(type), raw_resource]
71
+ else
72
+ [nil, nil]
73
+ end
74
+ end
75
+
76
+ def service_from_type type
77
+ LEGACY_TYPE_TO_SERVICE.each do |pattern, service|
78
+ return service if pattern =~ type
79
+ end
80
+ nil
81
+ end
82
+
83
+ def construct_cloud_event context, data
84
+ source, subject = convert_source context[:service], context[:resource]
85
+ type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
86
+ return nil unless type && source
87
+ CloudEvents::Event.new id: context[:id],
88
+ source: source,
89
+ type: type,
90
+ spec_version: "1.0",
91
+ data_content_type: "application/json",
92
+ data: data,
93
+ subject: subject,
94
+ time: context[:timestamp]
95
+ end
96
+
97
+ def convert_source service, resource
98
+ if service == "storage.googleapis.com"
99
+ match = %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}.match resource
100
+ return [nil, nil] unless match
101
+ ["//#{service}/#{match[1]}", match[2]]
102
+ else
103
+ ["//#{service}/#{resource}", nil]
104
+ end
105
+ end
106
+
107
+ LEGACY_TYPE_TO_SERVICE = {
108
+ %r{^providers/cloud\.firestore/} => "firestore.googleapis.com",
109
+ %r{^providers/cloud\.pubsub/} => "pubsub.googleapis.com",
110
+ %r{^providers/cloud\.storage/} => "storage.googleapis.com",
111
+ %r{^providers/firebase\.auth/} => "firebase.googleapis.com",
112
+ %r{^providers/google\.firebase} => "firebase.googleapis.com"
113
+ }.freeze
114
+
115
+ LEGACY_TYPE_TO_CE_TYPE = {
116
+ "google.pubsub.topic.publish" => "google.cloud.pubsub.topic.v1.messagePublished",
117
+ "providers/cloud.pubsub/eventTypes/topic.publish" => "google.cloud.pubsub.topic.v1.messagePublished",
118
+ "google.storage.object.finalize" => "google.cloud.storage.object.v1.finalized",
119
+ "google.storage.object.delete" => "google.cloud.storage.object.v1.deleted",
120
+ "google.storage.object.archive" => "google.cloud.storage.object.v1.archived",
121
+ "google.storage.object.metadataUpdate" => "google.cloud.storage.object.v1.metadataUpdated",
122
+ "providers/cloud.firestore/eventTypes/document.write" => "google.cloud.firestore.document.v1.written",
123
+ "providers/cloud.firestore/eventTypes/document.create" => "google.cloud.firestore.document.v1.created",
124
+ "providers/cloud.firestore/eventTypes/document.update" => "google.cloud.firestore.document.v1.updated",
125
+ "providers/cloud.firestore/eventTypes/document.delete" => "google.cloud.firestore.document.v1.deleted",
126
+ "providers/firebase.auth/eventTypes/user.create" => "google.firebase.auth.user.v1.created",
127
+ "providers/firebase.auth/eventTypes/user.delete" => "google.firebase.auth.user.v1.deleted",
128
+ "providers/google.firebase.analytics/eventTypes/event.log" => "google.firebase.analytics.log.v1.written",
129
+ "providers/google.firebase.database/eventTypes/ref.create" => "google.firebase.database.document.v1.created",
130
+ "providers/google.firebase.database/eventTypes/ref.write" => "google.firebase.database.document.v1.written",
131
+ "providers/google.firebase.database/eventTypes/ref.update" => "google.firebase.database.document.v1.updated",
132
+ "providers/google.firebase.database/eventTypes/ref.delete" => "google.firebase.database.document.v1.deleted",
133
+ "providers/cloud.storage/eventTypes/object.change" => "google.cloud.storage.object.v1.finalized"
134
+ }.freeze
135
+ end
136
+ end
@@ -76,31 +76,10 @@ module FunctionsFramework
76
76
  end
77
77
 
78
78
  ##
79
- # Add a CloudEvent function to the registry.
80
- #
81
- # You must provide a name for the function, and a block that implemets the
82
- # function. The block should take two arguments: the event _data_ and the
83
- # event _context_. Any return value is ignored.
84
- #
85
- # The event data argument will be one of the following types:
86
- # * A `String` (with encoding `ASCII-8BIT`) if the data is in the form of
87
- # binary data. You may choose to perform additional interpretation of
88
- # the binary data using information in the content type provided by the
89
- # context argument.
90
- # * Any data type that can be represented in JSON (i.e. `String`,
91
- # `Integer`, `Array`, `Hash`, `true`, `false`, or `nil`) if the event
92
- # came with a JSON payload. The content type may also be set in the
93
- # context if the data is a String.
94
- #
95
- # The context argument will be of type {FunctionsFramework::CloudEvents::Event},
96
- # and will contain CloudEvents context attributes such as `id` and `type`.
79
+ # This is an obsolete interface that defines an event function taking two
80
+ # arguments (data and context) rather than one.
97
81
  #
98
- # See also {#add_cloud_event} which creates a function that takes a single
99
- # argument of type {FunctionsFramework::CloudEvents::Event}.
100
- #
101
- # @param name [String] The function name
102
- # @param block [Proc] The function code as a proc
103
- # @return [self]
82
+ # @deprecated Use {Registry#add_cloud_event} instead.
104
83
  #
105
84
  def add_event name, &block
106
85
  name = name.to_s
@@ -118,9 +97,6 @@ module FunctionsFramework
118
97
  # function. The block should take _one_ argument: the event object of type
119
98
  # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
120
99
  #
121
- # See also {#add_event} which creates a function that takes data and
122
- # context as separate arguments.
123
- #
124
100
  # @param name [String] The function name
125
101
  # @param block [Proc] The function code as a proc
126
102
  # @return [self]
@@ -212,7 +212,7 @@ module FunctionsFramework
212
212
  # @param bind_addr [String,nil]
213
213
  #
214
214
  def bind_addr= bind_addr
215
- @bind_addr = bind_addr || ::ENV["BIND_ADDR"] || "0.0.0.0"
215
+ @bind_addr = bind_addr || ::ENV["FUNCTION_BIND_ADDR"] || "0.0.0.0"
216
216
  end
217
217
 
218
218
  ##
@@ -228,7 +228,7 @@ module FunctionsFramework
228
228
  # @param min_threads [Integer,nil]
229
229
  #
230
230
  def min_threads= min_threads
231
- @min_threads = (min_threads || ::ENV["MIN_THREADS"])&.to_i
231
+ @min_threads = (min_threads || ::ENV["FUNCTION_MIN_THREADS"])&.to_i
232
232
  end
233
233
 
234
234
  ##
@@ -236,7 +236,7 @@ module FunctionsFramework
236
236
  # @param max_threads [Integer,nil]
237
237
  #
238
238
  def max_threads= max_threads
239
- @max_threads = (max_threads || ::ENV["MAX_THREADS"])&.to_i
239
+ @max_threads = (max_threads || ::ENV["FUNCTION_MAX_THREADS"])&.to_i
240
240
  end
241
241
 
242
242
  ##
@@ -244,8 +244,12 @@ module FunctionsFramework
244
244
  # @param show_error_details [Boolean,nil]
245
245
  #
246
246
  def show_error_details= show_error_details
247
- val = show_error_details.nil? ? ::ENV["DETAILED_ERRORS"] : show_error_details
248
- @show_error_details = val ? true : false
247
+ @show_error_details =
248
+ if show_error_details.nil?
249
+ !::ENV["FUNCTION_DETAILED_ERRORS"].to_s.empty?
250
+ else
251
+ show_error_details ? true : false
252
+ end
249
253
  end
250
254
 
251
255
  ##
@@ -315,15 +319,15 @@ module FunctionsFramework
315
319
 
316
320
  ## @private
317
321
  class AppBase
318
- BLACKLISTED_PATHS = ["/favicon.ico", "/robots.txt"].freeze
322
+ EXCLUDED_PATHS = ["/favicon.ico", "/robots.txt"].freeze
319
323
 
320
324
  def initialize config
321
325
  @config = config
322
326
  end
323
327
 
324
- def blacklisted_path? env
328
+ def excluded_path? env
325
329
  path = env[::Rack::SCRIPT_NAME].to_s + env[::Rack::PATH_INFO].to_s
326
- BLACKLISTED_PATHS.include? path
330
+ EXCLUDED_PATHS.include? path
327
331
  end
328
332
 
329
333
  def interpret_response response
@@ -335,15 +339,13 @@ module FunctionsFramework
335
339
  when ::String
336
340
  string_response response, "text/plain", 200
337
341
  when ::Hash
338
- json = ::JSON.dump response
339
- string_response json, "application/json", 200
342
+ string_response ::JSON.dump(response), "application/json", 200
343
+ when CloudEvents::CloudEventsError
344
+ cloud_events_error_response response
340
345
  when ::StandardError
341
- error = error_message response
342
- string_response error, "text/plain", 500
346
+ error_response "#{response.class}: #{response.message}\n#{response.backtrace}\n"
343
347
  else
344
- e = ::StandardError.new "Unexpected response type: #{response.class}"
345
- error = error_message e
346
- string_response error, "text/plain", 500
348
+ error_response "Unexpected response type: #{response.class}"
347
349
  end
348
350
  end
349
351
 
@@ -359,20 +361,15 @@ module FunctionsFramework
359
361
  [status, headers, [string]]
360
362
  end
361
363
 
362
- def error_message error
363
- if @config.show_error_details?
364
- "#{error.class}: #{error.message}\n#{error.backtrace}\n"
365
- else
366
- "Unexpected internal error"
367
- end
364
+ def cloud_events_error_response error
365
+ @config.logger.warn error
366
+ string_response "#{error.class}: #{error.message}", "text/plain", 400
368
367
  end
369
368
 
370
- def usage_message error
371
- if @config.show_error_details?
372
- "Failed to decode CloudEvent: #{error.inspect}"
373
- else
374
- "Failed to decode CloudEvent"
375
- end
369
+ def error_response message
370
+ @config.logger.error message
371
+ message = "Unexpected internal error" unless @config.show_error_details?
372
+ string_response message, "text/plain", 500
376
373
  end
377
374
  end
378
375
 
@@ -384,7 +381,7 @@ module FunctionsFramework
384
381
  end
385
382
 
386
383
  def call env
387
- return notfound_response if blacklisted_path? env
384
+ return notfound_response if excluded_path? env
388
385
  response =
389
386
  begin
390
387
  logger = env["rack.logger"] = @config.logger
@@ -392,7 +389,6 @@ module FunctionsFramework
392
389
  logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
393
390
  @function.call request
394
391
  rescue ::StandardError => e
395
- logger.warn e
396
392
  e
397
393
  end
398
394
  interpret_response response
@@ -404,33 +400,45 @@ module FunctionsFramework
404
400
  def initialize function, config
405
401
  super config
406
402
  @function = function
403
+ @cloud_events = CloudEvents::HttpBinding.default
404
+ @legacy_events = LegacyEventConverter.new
407
405
  end
408
406
 
409
407
  def call env
410
- return notfound_response if blacklisted_path? env
408
+ return notfound_response if excluded_path? env
411
409
  logger = env["rack.logger"] = @config.logger
412
- event =
413
- begin
414
- CloudEvents.decode_rack_env env
415
- rescue ::StandardError => e
416
- e
417
- end
410
+ event = decode_event env
418
411
  response =
419
- if event.is_a? CloudEvents::Event
420
- logger.info "FunctionsFramework: Handling CloudEvent"
421
- begin
422
- @function.call event
423
- "ok"
424
- rescue ::StandardError => e
425
- logger.warn e
426
- e
427
- end
412
+ case event
413
+ when CloudEvents::Event
414
+ handle_cloud_event event, logger
415
+ when ::Array
416
+ CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
417
+ when CloudEvents::CloudEventsError
418
+ event
428
419
  else
429
- logger.warn e.inspect
430
- string_response usage_message(e), "text/plain", 400
420
+ raise "Unexpected event type: #{event.class}"
431
421
  end
432
422
  interpret_response response
433
423
  end
424
+
425
+ private
426
+
427
+ def decode_event env
428
+ @cloud_events.decode_rack_env(env) ||
429
+ @legacy_events.decode_rack_env(env) ||
430
+ raise(CloudEvents::HttpContentError, "Unrecognized event format")
431
+ rescue CloudEvents::CloudEventsError => e
432
+ e
433
+ end
434
+
435
+ def handle_cloud_event event, logger
436
+ logger.info "FunctionsFramework: Handling CloudEvent"
437
+ @function.call event
438
+ "ok"
439
+ rescue ::StandardError => e
440
+ e
441
+ end
434
442
  end
435
443
  end
436
444
  end