lib/functions_framework/cli.rb (160 lines of code) (raw):
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "logger"
require "optparse"
require "functions_framework"
module FunctionsFramework
##
# Implementation of the functions-framework-ruby executable.
#
class CLI
##
# The default logging level, if not given in the environment variable.
# @return [Integer]
#
DEFAULT_LOGGING_LEVEL = ::Logger::Severity::INFO
##
# Create a new CLI, setting arguments to their defaults.
#
def initialize
@target = ::ENV["FUNCTION_TARGET"] || ::FunctionsFramework::DEFAULT_TARGET
@source = ::ENV["FUNCTION_SOURCE"] || ::FunctionsFramework::DEFAULT_SOURCE
@env = nil
@port = nil
@pidfile = nil
@bind = nil
@min_threads = nil
@max_threads = nil
@detailed_errors = nil
@signature_type = ::ENV["FUNCTION_SIGNATURE_TYPE"]
@logging_level = init_logging_level
@what_to_do = nil
@error_message = nil
@exit_code = 0
end
##
# Determine if an error has occurred
#
# @return [boolean]
#
def error?
!@error_message.nil?
end
##
# @return [Integer] The current exit status.
#
attr_reader :exit_code
##
# @return [String] The current error message.
# @return [nil] if no error has occurred.
#
attr_reader :error_message
##
# @return [String] The pidfile.
# @return [nil] if not running.
#
attr_reader :pidfile
##
# Parse the given command line arguments.
# Exits if argument parsing failed.
#
# @param argv [Array<String>]
# @return [self]
#
def parse_args argv # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
@option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
op.on "-t", "--target TARGET",
"Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
@target = val
end
op.on "-s", "--source SOURCE",
"Set the source file to load (defaults to #{DEFAULT_SOURCE})" do |val|
@source = val
end
op.on "--signature-type TYPE",
"Asserts that the function has the given signature type. " \
"Supported values are 'http' and 'cloudevent'." do |val|
@signature_type = val
end
op.on "-P", "--pidfile PIDFILE", "Set the pidfile for the server (defaults to puma.pid)" do |val|
@pidfile = val
end
op.on "-p", "--port PORT", "Set the port to listen to (defaults to 8080)" do |val|
@port = val.to_i
end
op.on "-b", "--bind BIND", "Set the address to bind to (defaults to 0.0.0.0)" do |val|
@bind = val
end
op.on "-e", "--environment ENV", "Set the Rack environment" do |val|
@env = val
end
op.on "--min-threads NUM", "Set the minimum thread pool size" do |val|
@min_threads = val
end
op.on "--max-threads NUM", "Set the maximum thread pool size" do |val|
@max_threads = val
end
op.on "--[no-]detailed-errors", "Set whether to show error details" do |val|
@detailed_errors = val
end
op.on "--verify", "Verify the app only, but do not run the server." do
@what_to_do ||= :verify
end
op.on "-v", "--verbose", "Increase log verbosity" do
@logging_level -= 1
end
op.on "-q", "--quiet", "Decrease log verbosity" do
@logging_level += 1
end
op.on "--version", "Display the framework version" do
@what_to_do ||= :version
end
op.on "--help", "Display help" do
@what_to_do ||= :help
end
end
begin
@option_parser.parse! argv
error! "Unrecognized arguments: #{argv}\n#{@option_parser}", 2 unless argv.empty?
rescue ::OptionParser::ParseError => e
error! "#{e.message}\n#{@option_parser}", 2
end
self
end
##
# Perform the requested function.
#
# * If the `--version` flag was given, display the version.
# * If the `--help` flag was given, display online help.
# * If the `--verify` flag was given, load and verify the function,
# displaying any errors, then exit without starting a server.
# * Otherwise, start the configured server and block until it stops.
#
# @return [self]
#
def run
return self if error?
case @what_to_do
when :version
puts ::FunctionsFramework::VERSION
when :help
puts @option_parser
when :verify
begin
load_function
puts "OK"
rescue ::StandardError => e
error! e.message
end
else
begin
start_server.wait_until_stopped
rescue ::StandardError => e
error! e.message
end
end
self
end
##
# Finish the CLI, displaying any error status and exiting with the current
# exit code. Never returns.
#
def complete
warn @error_message if @error_message
exit @exit_code
end
##
# Load the source and get and verify the requested function.
# If a validation error occurs, raise an exception.
#
# @return [FunctionsFramework::Function]
#
# @private
#
def load_function
::FunctionsFramework.logger.level = @logging_level
::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION}"
::ENV["FUNCTION_TARGET"] = @target
::ENV["FUNCTION_SOURCE"] = @source
::ENV["FUNCTION_SIGNATURE_TYPE"] = @signature_type
::FunctionsFramework.logger.info "FunctionsFramework: Loading functions from #{@source.inspect}..."
load @source
::FunctionsFramework.logger.info "FunctionsFramework: Looking for function name #{@target.inspect}..."
function = ::FunctionsFramework.global_registry[@target]
raise "Undefined function: #{@target.inspect}" if function.nil?
unless @signature_type.nil? ||
(@signature_type == "http" && function.type == :http) ||
(@signature_type == "http" && function.type == :typed) ||
(["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event)
raise "Function #{@target.inspect} does not match type #{@signature_type}"
end
function
end
##
# Start the configured server and return the running server object.
# If a validation error occurs, raise an exception.
#
# @return [FunctionsFramework::Server]
#
# @private
#
def start_server
function = load_function
::FunctionsFramework.logger.info "FunctionsFramework: Starting server..."
::FunctionsFramework.start function do |config|
config.rack_env = @env
config.port = @port
config.pidfile = @pidfile
config.bind_addr = @bind
config.show_error_details = @detailed_errors
config.min_threads = @min_threads
config.max_threads = @max_threads
end
end
private
def init_logging_level
level_name = ::ENV["FUNCTION_LOGGING_LEVEL"].to_s.upcase.to_sym
::Logger::Severity.const_get level_name
rescue ::NameError
DEFAULT_LOGGING_LEVEL
end
##
# Set the error status.
# @param message [String] Error message.
# @param code [Integer] Exit code, defaults to 1.
#
def error! message, code = 1
@error_message = message
@exit_code = code
end
end
end