functions_framework 0.5.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +5 -5
- data/bin/functions-framework +4 -1
- data/bin/functions-framework-ruby +1 -1
- data/docs/deploying-functions.md +7 -11
- data/docs/overview.md +4 -4
- data/docs/testing-functions.md +50 -0
- data/docs/writing-functions.md +246 -7
- data/lib/functions_framework.rb +30 -3
- data/lib/functions_framework/cli.rb +98 -23
- data/lib/functions_framework/function.rb +190 -48
- data/lib/functions_framework/legacy_event_converter.rb +49 -22
- data/lib/functions_framework/registry.rb +34 -11
- data/lib/functions_framework/server.rb +22 -17
- data/lib/functions_framework/testing.rb +106 -18
- data/lib/functions_framework/version.rb +1 -1
- metadata +8 -8
data/lib/functions_framework.rb
CHANGED
@@ -166,8 +166,30 @@ module FunctionsFramework
|
|
166
166
|
end
|
167
167
|
|
168
168
|
##
|
169
|
-
#
|
170
|
-
#
|
169
|
+
# Define a server startup task. This is useful for initializing shared
|
170
|
+
# resources that should be accessible across all function invocations in
|
171
|
+
# this Ruby VM.
|
172
|
+
#
|
173
|
+
# Startup tasks are run just before a server starts. All startup tasks are
|
174
|
+
# guaranteed to complete before any function executes. However, they are
|
175
|
+
# run only when preparing to run functions. They are not run, for example,
|
176
|
+
# if an app is loaded to verify its integrity during deployment.
|
177
|
+
#
|
178
|
+
# Startup tasks are passed the {FunctionsFramework::Function} identifying
|
179
|
+
# the function to execute, and have no return value.
|
180
|
+
#
|
181
|
+
# @param block [Proc] The startup task
|
182
|
+
# @return [self]
|
183
|
+
#
|
184
|
+
def on_startup &block
|
185
|
+
global_registry.add_startup_task(&block)
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Run startup tasks, then start the functions framework server in the
|
191
|
+
# background. The startup tasks and target function will be looked up in
|
192
|
+
# the global registry.
|
171
193
|
#
|
172
194
|
# @param target [FunctionsFramework::Function,String] The function to run,
|
173
195
|
# or the name of the function to look up in the global registry.
|
@@ -183,7 +205,12 @@ module FunctionsFramework
|
|
183
205
|
function = global_registry[target]
|
184
206
|
raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
|
185
207
|
end
|
186
|
-
|
208
|
+
globals = function.populate_globals
|
209
|
+
server = Server.new function, globals, &block
|
210
|
+
global_registry.startup_tasks.each do |task|
|
211
|
+
task.call function, globals: globals, logger: server.config.logger
|
212
|
+
end
|
213
|
+
globals.freeze
|
187
214
|
server.respond_to_signals
|
188
215
|
server.start
|
189
216
|
end
|
@@ -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.
|
@@ -51,8 +74,8 @@ module FunctionsFramework
|
|
51
74
|
# @param argv [Array<String>]
|
52
75
|
# @return [self]
|
53
76
|
#
|
54
|
-
def parse_args argv # rubocop:disable Metrics/MethodLength
|
55
|
-
option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
|
77
|
+
def parse_args argv # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
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
|
-
|
95
|
-
exit
|
123
|
+
@what_to_do ||= :help
|
96
124
|
end
|
97
125
|
end
|
98
|
-
|
99
|
-
|
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
|
-
#
|
105
|
-
#
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
#
|
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::
|
183
|
+
# @return [FunctionsFramework::Function]
|
125
184
|
#
|
126
185
|
# @private
|
127
186
|
#
|
128
|
-
def
|
187
|
+
def load_function
|
129
188
|
::FunctionsFramework.logger.level = @logging_level
|
130
|
-
::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION}
|
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
|
-
#
|
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
|
-
|
168
|
-
|
241
|
+
def error! message, code = 1
|
242
|
+
@error_message = message
|
243
|
+
@exit_code = code
|
169
244
|
end
|
170
245
|
end
|
171
246
|
end
|
@@ -18,44 +18,91 @@ module FunctionsFramework
|
|
18
18
|
#
|
19
19
|
# A function has a name, a type, and an implementation.
|
20
20
|
#
|
21
|
+
# ## Function implementations
|
22
|
+
#
|
21
23
|
# The implementation in general is an object that responds to the `call`
|
22
|
-
# method.
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
24
|
+
# method.
|
25
|
+
#
|
26
|
+
# * For a function of type `:http`, the `call` method takes a single
|
27
|
+
# `Rack::Request` argument and returns one of various HTTP response
|
28
|
+
# types. See {FunctionsFramework::Registry.add_http}.
|
29
|
+
# * For a function of type `:cloud_event`, the `call` method takes a single
|
30
|
+
# [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event)
|
31
|
+
# argument, and does not return a value. See
|
32
|
+
# {FunctionsFramework::Registry.add_cloud_event}.
|
33
|
+
# * For a function of type `:startup_task`, the `call` method takes a
|
34
|
+
# single {FunctionsFramework::Function} argument, and does not return a
|
35
|
+
# value. See {FunctionsFramework::Registry.add_startup_task}.
|
29
36
|
#
|
30
|
-
#
|
31
|
-
# every function execution. Note that this means it may be called multiple
|
32
|
-
# times concurrently in separate threads.
|
37
|
+
# The implementation can be specified in one of three ways:
|
33
38
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
39
|
+
# * A callable object can be passed in the `callable` keyword argument. The
|
40
|
+
# object's `call` method will be invoked for every function execution.
|
41
|
+
# Note that this means it may be called multiple times concurrently in
|
42
|
+
# separate threads.
|
43
|
+
# * A callable _class_ can be passed in the `callable` keyword argument.
|
44
|
+
# This class should subclass {FunctionsFramework::Function::Callable} and
|
45
|
+
# define the `call` method. A separate instance of this class will be
|
46
|
+
# created for each function invocation.
|
47
|
+
# * A block can be provided. It will be used to define the `call` method in
|
48
|
+
# an anonymous subclass of {FunctionsFramework::Function::Callable}.
|
49
|
+
# Thus, providing a block is really just syntactic sugar for providing a
|
50
|
+
# class. (This means, for example, that the `return` keyword will work
|
51
|
+
# as expected within the block because it is treated as a method.)
|
40
52
|
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
53
|
+
# When the implementation is provided as a callable class or block, it is
|
54
|
+
# executed in the context of a {FunctionsFramework::Function::Callable}
|
55
|
+
# object. This object provides a convenience accessor for the Logger, and
|
56
|
+
# access to _globals_, which are data defined by the application startup
|
57
|
+
# process and available to each function invocation. Typically, globals are
|
58
|
+
# used for shared global resources such as service connections and clients.
|
47
59
|
#
|
48
60
|
class Function
|
61
|
+
##
|
62
|
+
# Create a new HTTP function definition.
|
63
|
+
#
|
64
|
+
# @param name [String] The function name
|
65
|
+
# @param callable [Class,#call] A callable object or class.
|
66
|
+
# @param block [Proc] The function code as a block.
|
67
|
+
# @return [FunctionsFramework::Function]
|
68
|
+
#
|
69
|
+
def self.http name, callable: nil, &block
|
70
|
+
new name, :http, callable: callable, &block
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Create a new CloudEvents function definition.
|
75
|
+
#
|
76
|
+
# @param name [String] The function name
|
77
|
+
# @param callable [Class,#call] A callable object or class.
|
78
|
+
# @param block [Proc] The function code as a block.
|
79
|
+
# @return [FunctionsFramework::Function]
|
80
|
+
#
|
81
|
+
def self.cloud_event name, callable: nil, &block
|
82
|
+
new name, :cloud_event, callable: callable, &block
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Create a new startup task function definition.
|
87
|
+
#
|
88
|
+
# @param callable [Class,#call] A callable object or class.
|
89
|
+
# @param block [Proc] The function code as a block.
|
90
|
+
# @return [FunctionsFramework::Function]
|
91
|
+
#
|
92
|
+
def self.startup_task callable: nil, &block
|
93
|
+
new nil, :startup_task, callable: callable, &block
|
94
|
+
end
|
95
|
+
|
49
96
|
##
|
50
97
|
# Create a new function definition.
|
51
98
|
#
|
52
99
|
# @param name [String] The function name
|
53
|
-
# @param type [Symbol] The type of function. Valid types are `:http
|
54
|
-
# `:cloud_event`.
|
100
|
+
# @param type [Symbol] The type of function. Valid types are `:http`,
|
101
|
+
# `:cloud_event`, and `:startup_task`.
|
55
102
|
# @param callable [Class,#call] A callable object or class.
|
56
103
|
# @param block [Proc] The function code as a block.
|
57
104
|
#
|
58
|
-
def initialize name, type, callable
|
105
|
+
def initialize name, type, callable: nil, &block
|
59
106
|
@name = name
|
60
107
|
@type = type
|
61
108
|
@callable = @callable_class = nil
|
@@ -64,7 +111,7 @@ module FunctionsFramework
|
|
64
111
|
elsif callable.is_a? ::Class
|
65
112
|
@callable_class = callable
|
66
113
|
elsif block_given?
|
67
|
-
@callable_class = ::Class.new
|
114
|
+
@callable_class = ::Class.new Callable do
|
68
115
|
define_method :call, &block
|
69
116
|
end
|
70
117
|
else
|
@@ -83,18 +130,60 @@ module FunctionsFramework
|
|
83
130
|
attr_reader :type
|
84
131
|
|
85
132
|
##
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
133
|
+
# Populate the given globals hash with this function's info.
|
134
|
+
#
|
135
|
+
# @param globals [Hash] Initial globals hash (optional).
|
136
|
+
# @return [Hash] A new globals hash with this function's info included.
|
137
|
+
#
|
138
|
+
def populate_globals globals = nil
|
139
|
+
result = { function_name: name, function_type: type }
|
140
|
+
result.merge! globals if globals
|
141
|
+
result
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Call the function given a set of arguments. Set the given logger and/or
|
146
|
+
# globals in the context if the callable supports it.
|
147
|
+
#
|
148
|
+
# If the given arguments exceeds what the function will accept, the args
|
149
|
+
# are silently truncated. However, if the function requires more arguments
|
150
|
+
# than are provided, an ArgumentError is raised.
|
151
|
+
#
|
152
|
+
# @param args [Array] Argument to pass to the function.
|
153
|
+
# @param logger [Logger] Logger for use by function executions.
|
154
|
+
# @param globals [Hash] Globals for the function execution context
|
155
|
+
# @return [Object] The function return value.
|
156
|
+
#
|
157
|
+
def call *args, globals: nil, logger: nil
|
158
|
+
callable = @callable || @callable_class.new(globals: globals, logger: logger)
|
159
|
+
params = callable.method(:call).parameters.map(&:first)
|
160
|
+
unless params.include? :rest
|
161
|
+
max_params = params.count(:req) + params.count(:opt)
|
162
|
+
args = args.take max_params
|
163
|
+
end
|
164
|
+
callable.call(*args)
|
165
|
+
end
|
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
|
98
187
|
end
|
99
188
|
|
100
189
|
##
|
@@ -102,29 +191,82 @@ module FunctionsFramework
|
|
102
191
|
#
|
103
192
|
# An object of this class is `self` while a function block is running.
|
104
193
|
#
|
105
|
-
class
|
194
|
+
class Callable
|
106
195
|
##
|
107
196
|
# Create a callable object with the given context.
|
108
197
|
#
|
109
|
-
# @param
|
110
|
-
#
|
111
|
-
# implementations should be prepared to accept any abritrary keys.
|
198
|
+
# @param globals [Hash] A set of globals available to the call.
|
199
|
+
# @param logger [Logger] A logger for use by the function call.
|
112
200
|
#
|
113
|
-
def initialize
|
114
|
-
@
|
201
|
+
def initialize globals: nil, logger: nil
|
202
|
+
@__globals = globals || {}
|
203
|
+
@__logger = logger || FunctionsFramework.logger
|
115
204
|
end
|
116
205
|
|
117
206
|
##
|
118
|
-
#
|
207
|
+
# Get the given named global.
|
208
|
+
#
|
209
|
+
# For most function calls, the following globals will be defined:
|
119
210
|
#
|
120
|
-
# * **:logger** (`Logger`) A logger for use by this function call.
|
121
211
|
# * **:function_name** (`String`) The name of the running function.
|
122
212
|
# * **:function_type** (`Symbol`) The type of the running function,
|
123
213
|
# either `:http` or `:cloud_event`.
|
124
214
|
#
|
125
|
-
#
|
215
|
+
# You can also set additional globals from a startup task.
|
216
|
+
#
|
217
|
+
# @param key [Symbol,String] The name of the global to get.
|
218
|
+
# @return [Object]
|
219
|
+
#
|
220
|
+
def global key
|
221
|
+
value = @__globals[key]
|
222
|
+
value = value.value if LazyGlobal === value
|
223
|
+
value
|
224
|
+
end
|
225
|
+
|
226
|
+
##
|
227
|
+
# Set a global. This can be called from startup tasks, but the globals
|
228
|
+
# are frozen when the server starts, so this call will raise an exception
|
229
|
+
# if called from a normal function.
|
230
|
+
#
|
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
|
126
251
|
#
|
127
|
-
|
252
|
+
# @param key [Symbol,String]
|
253
|
+
# @param block [Proc] A block that lazily computes a value
|
254
|
+
# @yieldreturn [Object] The value
|
255
|
+
# @return [self]
|
256
|
+
#
|
257
|
+
def set_global key, value = nil, &block
|
258
|
+
@__globals[key] = block ? LazyGlobal.new(block) : value
|
259
|
+
self
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# A logger for use by this call.
|
264
|
+
#
|
265
|
+
# @return [Logger]
|
266
|
+
#
|
267
|
+
def logger
|
268
|
+
@__logger
|
269
|
+
end
|
128
270
|
end
|
129
271
|
end
|
130
272
|
end
|