functions_framework 0.4.0 → 0.6.0

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