functions_framework 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d98d20b3b8e7ab30b0ef44dfee51e6df6ab8f1f2816ee72e50ecaaca099e0fe5
4
- data.tar.gz: 1ee993fdaea21fff0c2d6f551d4cea3d67f0ce0264ee0d4fdf6f1c656c7e8211
3
+ metadata.gz: d09328e219e183d857a5b0e5cbb4a9aac77dcb2b6deea96ecb4909729aac5e46
4
+ data.tar.gz: 2ffdd21b22c08193b64610ac42ee05f6b5627c25b99ee6d2d45eb52408438e33
5
5
  SHA512:
6
- metadata.gz: 698ee184e253b7cf071bcf3d1065ed18f06c93e58fae662fbc72ddffa5ea8c92a051d7045fc2afd383f114ac5a46d8d72759eac93b283bd43d4d263c163bd050
7
- data.tar.gz: 6bb25e647fc7275311a3dab8cb455214dc58c074d45c5ac17bbf967381d4903e7defeedf74840a5cae045b7d6dc0bd31cbf8e539a8294d43764ec264ec9b94a9
6
+ metadata.gz: 93cf161a26eee8349e99ec7012698374206e333099c94ecff07609bdefe9f7a33bf331aa3a0b305fd005e0a89ba9988bcc3ed3ce4c2c1edadd587975561b5fe8
7
+ data.tar.gz: 64eea2572a244dca261c090b6e51445b2f59ad2ad985edb29dc2a52aaa902fd9d9afa44e65e6ca51a6653c16d5532ace49cb9a6defd09c45195d9d81e9238707
data/.yardopts CHANGED
@@ -7,5 +7,4 @@
7
7
  ./lib/functions_framework.rb
8
8
  -
9
9
  README.md
10
- CONTRIBUTING.md
11
10
  CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -1,2 +1,5 @@
1
1
  # Changelog
2
2
 
3
+ ### v0.1.0 / 2020-01-30
4
+
5
+ * Initial release
data/README.md CHANGED
@@ -1,12 +1,61 @@
1
1
  # Functions Framework
2
2
 
3
+ An open source framework for writing lightweight, portable Ruby functions that
4
+ run in a serverless environment. Functions written to this Framework will run
5
+ in many different environments, including:
6
+
7
+ * [Google Cloud Functions](https://cloud.google.com/functions) *(coming soon)*
8
+ * [Cloud Run or Cloud Run for Anthos](https://cloud.google.com/run)
9
+ * Any other [Knative](https://github.com/knative)-based environment
10
+ * Your local development machine
11
+
12
+ The framework allows you to go from:
13
+
14
+ ```ruby
15
+ FunctionsFramework.http do |request|
16
+ "Hello, world!\n"
17
+ end
18
+ ```
19
+
20
+ To:
21
+
22
+ ```sh
23
+ curl http://my-url
24
+ # Output: Hello, world!
25
+ ```
26
+
27
+ All without needing to worry about writing an HTTP server or complicated
28
+ request handling logic.
29
+
30
+ For more information about the Functions Framework, see
31
+ https://github.com/GoogleCloudPlatform/functions-framework
32
+
33
+ ## Features
34
+
35
+ * Define named functions using normal Ruby constructs.
36
+ * Invoke functions in response to requests.
37
+ * Automatically unmarshal events conforming to the
38
+ [CloudEvents](https://cloudevents.io) spec.
39
+ * Spin up a local development server for quick testing.
40
+ * Integrate with standard Ruby libraries such as Rack and Minitest.
41
+ * Portable between serverless platforms.
42
+
3
43
  ## Installation
4
44
 
45
+ Install the Functions Framework via Rubygems:
46
+
47
+ ```sh
48
+ gem install functions_framework
5
49
  ```
6
- $ gem install functions_framework
50
+
51
+ Or add it to your Gemfile for installation using Bundler:
52
+
53
+ ```ruby
54
+ # Gemfile
55
+ gem "functions_framework", "~> 0.1"
7
56
  ```
8
57
 
9
- ## Supported Ruby Versions
58
+ ### Supported Ruby versions
10
59
 
11
60
  This library is supported on Ruby 2.4+.
12
61
 
@@ -16,3 +65,154 @@ in security maintenance, and not end of life. Currently, this means Ruby 2.4
16
65
  and later. Older versions of Ruby _may_ still work, but are unsupported and not
17
66
  recommended. See https://www.ruby-lang.org/en/downloads/branches/ for details
18
67
  about the Ruby support schedule.
68
+
69
+ ## Quickstart
70
+
71
+ ### Running Hello World on your local machine
72
+
73
+ Create a `Gemfile` listing the Functions Framework as a dependency:
74
+
75
+ ```ruby
76
+ # Gemfile
77
+ source "https://rubygems.org"
78
+ gem "functions_framework", "~> 0.1"
79
+ ```
80
+
81
+ Create a file called `app.rb` and include the following code. This defines a
82
+ simple function called "hello".
83
+
84
+ ```ruby
85
+ # app.rb
86
+ require "functions_framework"
87
+
88
+ FunctionsFramework.http("hello") do |request|
89
+ "Hello, world!\n"
90
+ end
91
+ ```
92
+
93
+ Install the bundle, and start the framework. This spins up a local web server
94
+ running your "hello" function:
95
+
96
+ ```sh
97
+ bundle install
98
+ # ...installs the functions_framework gem and other dependencies
99
+ bundle exec functions-framework --target hello
100
+ # ...starts the web server in the foreground
101
+ ```
102
+
103
+ In a separate shell, you can send requests to this function using curl:
104
+
105
+ ```sh
106
+ curl localhost:8080
107
+ # Output: Hello, world!
108
+ ```
109
+
110
+ Stop the web server with `CTRL+C`.
111
+
112
+ ### Deploying a function to Google Cloud Functions
113
+
114
+ (Google Cloud Functions does not yet support the Ruby Function Framework)
115
+
116
+ ### Deploying a function to Cloud Run
117
+
118
+ Create a `Dockerfile` for your function:
119
+
120
+ ```dockerfile
121
+ # Dockerfile
122
+ FROM ruby:2.6
123
+
124
+ WORKDIR /app
125
+ COPY . .
126
+ RUN gem install --no-document bundler \
127
+ && bundle config --local frozen true \
128
+ && bundle install
129
+
130
+ ENTRYPOINT ["bundle", "exec", "functions-framework"]
131
+ CMD ["--target", "hello"]
132
+ ```
133
+
134
+ Build your function into a Docker image:
135
+
136
+ ```sh
137
+ gcloud builds submit --tag=gcr.io/[YOUR-PROJECT]/hello:build-1
138
+ ```
139
+
140
+ Deploy to Cloud Run:
141
+
142
+ ```sh
143
+ gcloud run deploy hello --image=gcr.io/[YOUR-PROJECT]/hello:build-1 \
144
+ --platform=managed --allow-unauthenticated --region=us-central1
145
+ ```
146
+
147
+ You can use a similar approach to deploy to any other Knative-based serverless
148
+ environment.
149
+
150
+ ### Responding to CloudEvents
151
+
152
+ You can also define a function that response to
153
+ [CloudEvents](https://cloudevents.io). The Functions Framework will handle
154
+ unmarshalling of the event data.
155
+
156
+ Change `app.rb` to read:
157
+
158
+ ```ruby
159
+ # app.rb
160
+ require "functions_framework"
161
+
162
+ FunctionsFramework.event("my-handler") do |data, context|
163
+ FunctionsFramework.logger.info "I received #{data.inspect}"
164
+ end
165
+ ```
166
+
167
+ Start up the framework with this new function:
168
+
169
+ ```sh
170
+ bundle install
171
+ bundle exec functions-framework --target my-handler
172
+ ```
173
+
174
+ In a separate shell, you can send a CloudEvent to this function using curl:
175
+
176
+ ```sh
177
+ curl --header "Content-Type: text/plain; charset=utf-8" \
178
+ --header "CE-ID: 12345" \
179
+ --header "CE-Source: curl" \
180
+ --header "CE-Type: com.example.test" \
181
+ --header "CE-Specversion: 1.0" \
182
+ --data "Hello, world!" \
183
+ http://localhost:8080
184
+ ```
185
+
186
+ CloudEvents functions do not return meaningful results, but you will see the
187
+ log message from the web server.
188
+
189
+ ### Configuring the Functions Framework
190
+
191
+ The Ruby Functions Framework recognizes the standard command line arguments to
192
+ the `functions-framework` executable. Each argument also corresponds to an
193
+ environment variable. If you specify both, the environment variable will be
194
+ ignored.
195
+
196
+ Command-line flag | Environment variable | Description
197
+ ----------------- | -------------------- | -----------
198
+ `--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080`
199
+ `--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function`
200
+ `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `app.rb` (in the current working directory)
201
+
202
+ Note: the flag `--signature-type` and corresponding environment variable
203
+ `FUNCTION_SIGNATURE_TYPE` are not used by the Ruby Function Framework, because
204
+ you specify the signature type when defining the function in the source.
205
+
206
+ The Ruby `functions-framework` executable also recognizes several additional
207
+ flags that can be used to control logging verbosity, binding, and other
208
+ parameters. For details, see the online help:
209
+
210
+ ```sh
211
+ functions-framework --help
212
+ ```
213
+
214
+ ## For more information
215
+
216
+ * See the `examples` directory for additional examples
217
+ * Consult https://rubydoc.info/gems/functions_framework for full reference
218
+ documentation.
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2020 Google LLC
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "functions_framework/cli"
18
+
19
+ ::FunctionsFramework::CLI.new.parse_args(::ARGV).run
@@ -0,0 +1,113 @@
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 "optparse"
16
+
17
+ require "functions_framework"
18
+
19
+ module FunctionsFramework
20
+ ##
21
+ # Implementation of the functions-framework executable.
22
+ #
23
+ class CLI
24
+ ##
25
+ # Create a new CLI, setting arguments to their defaults.
26
+ #
27
+ def initialize
28
+ @target = ::ENV["FUNCTION_TARGET"] || ::FunctionsFramework::DEFAULT_TARGET
29
+ @source = ::ENV["FUNCTION_SOURCE"] || ::FunctionsFramework::DEFAULT_SOURCE
30
+ @env = nil
31
+ @port = nil
32
+ @bind = nil
33
+ @min_threads = nil
34
+ @max_threads = nil
35
+ @detailed_errors = nil
36
+ end
37
+
38
+ ##
39
+ # Parse the given command line arguments.
40
+ # Exits if argument parsing failed.
41
+ #
42
+ # @param argv [Array<String>]
43
+ # @return [self]
44
+ #
45
+ def parse_args argv # rubocop:disable Metrics/MethodLength
46
+ option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
47
+ op.on "-t", "--target TARGET",
48
+ "Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
49
+ @target = val
50
+ end
51
+ op.on "-s", "--source SOURCE",
52
+ "Set the source file to load (defaults to #{DEFAULT_SOURCE})" do |val|
53
+ @source = val
54
+ end
55
+ op.on "-p", "--port PORT", "Set the port to listen to (defaults to 8080)" do |val|
56
+ @port = val.to_i
57
+ end
58
+ op.on "-b", "--bind BIND", "Set the address to bind to (defaults to 0.0.0.0)" do |val|
59
+ @bind = val
60
+ end
61
+ op.on "-e", "--environment ENV", "Set the Rack environment" do |val|
62
+ @env = val
63
+ end
64
+ op.on "--min-threads NUM", "Set the minimum threead pool size" do |val|
65
+ @min_threads = val
66
+ end
67
+ op.on "--max-threads NUM", "Set the maximum threead pool size" do |val|
68
+ @max_threads = val
69
+ end
70
+ op.on "--[no-]detailed-errors", "Set whether to show error details" do |val|
71
+ @detailed_errors = val
72
+ end
73
+ op.on "-v", "--verbose", "Increase log verbosity" do
74
+ ::FunctionsFramework.logger.level -= 1
75
+ end
76
+ op.on "-q", "--quiet", "Decrease log verbosity" do
77
+ ::FunctionsFramework.logger.level += 1
78
+ end
79
+ op.on "--help", "Display help" do
80
+ puts op
81
+ exit
82
+ end
83
+ end
84
+ option_parser.parse! argv
85
+ unless argv.empty?
86
+ warn "Unrecognized arguments: #{argv}"
87
+ puts op
88
+ exit 1
89
+ end
90
+ self
91
+ end
92
+
93
+ ##
94
+ # Run the configured server, and block until it stops.
95
+ # @return [self]
96
+ #
97
+ def run
98
+ FunctionsFramework.logger.info \
99
+ "FunctionsFramework: Loading functions from #{@source.inspect}..."
100
+ load @source
101
+ server = ::FunctionsFramework.start @target do |config|
102
+ config.rack_env = @env
103
+ config.port = @port
104
+ config.bind_addr = @bind
105
+ config.show_error_details = @detailed_errors
106
+ config.min_threads = @min_threads
107
+ config.max_threads = @max_threads
108
+ end
109
+ server.wait_until_stopped
110
+ self
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,59 @@
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
+ module FunctionsFramework
16
+ module CloudEvents
17
+ ##
18
+ # A content handler for the binary mode.
19
+ # See https://github.com/cloudevents/spec/blob/master/http-protocol-binding.md
20
+ #
21
+ module BinaryContent
22
+ class << self
23
+ ##
24
+ # Decode an event from the given Rack environment
25
+ #
26
+ # @param env [Hash] Rack environment hash
27
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType]
28
+ # the content type from the Rack environment
29
+ # @return [FunctionsFramework::CloudEvents::Event]
30
+ #
31
+ def decode_rack_env env, content_type
32
+ data = env["rack.input"]&.read
33
+ spec_version = interpret_header env, "HTTP_CE_SPECVERSION"
34
+ raise "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
35
+ Event.new \
36
+ id: interpret_header(env, "HTTP_CE_ID"),
37
+ source: interpret_header(env, "HTTP_CE_SOURCE"),
38
+ type: interpret_header(env, "HTTP_CE_TYPE"),
39
+ spec_version: spec_version,
40
+ data: data,
41
+ data_content_type: content_type,
42
+ data_schema: interpret_header(env, "HTTP_CE_DATASCHEMA"),
43
+ subject: interpret_header(env, "HTTP_CE_SUBJECT"),
44
+ time: interpret_header(env, "HTTP_CE_TIME")
45
+ end
46
+
47
+ private
48
+
49
+ def interpret_header env, key
50
+ escaped_value = env[key]
51
+ return nil if escaped_value.nil?
52
+ escaped_value.gsub(/%([0-9a-fA-F]{2})/) do
53
+ [$1.to_i(16)].pack "C" # rubocop:disable Style/PerlBackrefs
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,139 @@
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
+ module FunctionsFramework
16
+ module CloudEvents
17
+ ##
18
+ # A parsed content-type header.
19
+ #
20
+ # This object represents the information contained in a Content-Type,
21
+ # obtained by parsing the header according to RFC 2045.
22
+ #
23
+ # Case-insensitive fields, such as media_type and subtype, are normalized
24
+ # to lower case.
25
+ #
26
+ class ContentType
27
+ ##
28
+ # Parse the given header value
29
+ #
30
+ # @param string [String] Content-Type header value in RFC 2045 format
31
+ #
32
+ def initialize string
33
+ @string = string
34
+ # TODO: This handles simple cases but is not RFC-822 compliant.
35
+ sections = string.to_s.split ";"
36
+ media_type, subtype = sections.shift.split "/"
37
+ subtype_prefix, subtype_format = subtype.split "+"
38
+ @media_type = media_type.strip.downcase
39
+ @subtype = subtype.strip.downcase
40
+ @subtype_prefix = subtype_prefix.strip.downcase
41
+ @subtype_format = subtype_format&.strip&.downcase
42
+ @params = initialize_params sections
43
+ @canonical_string = "#{@media_type}/#{@subtype}" +
44
+ @params.map { |k, v| "; #{k}=#{v}" }.join
45
+ end
46
+
47
+ ##
48
+ # The original header content string
49
+ # @return [String]
50
+ #
51
+ attr_reader :string
52
+ alias to_s string
53
+
54
+ ##
55
+ # A "canonical" header content string with spacing and capitalization
56
+ # normalized.
57
+ # @return [String]
58
+ #
59
+ attr_reader :canonical_string
60
+
61
+ ##
62
+ # The media type.
63
+ # @return [String]
64
+ #
65
+ attr_reader :media_type
66
+
67
+ ##
68
+ # The entire content subtype (which could include an extension delimited
69
+ # by a plus sign)
70
+ # @return [String]
71
+ #
72
+ attr_reader :subtype
73
+
74
+ ##
75
+ # The portion of the content subtype before any plus sign.
76
+ # @return [String]
77
+ #
78
+ attr_reader :subtype_prefix
79
+
80
+ ##
81
+ # The portion of the content subtype after any plus sign, or nil if there
82
+ # is no plus sign in the subtype.
83
+ # @return [String,nil]
84
+ #
85
+ attr_reader :subtype_format
86
+
87
+ ##
88
+ # An array of parameters, each element as a two-element array of the
89
+ # parameter name and value.
90
+ # @return [Array<Array(String,String)>]
91
+ #
92
+ attr_reader :params
93
+
94
+ ##
95
+ # An array of values for the given parameter name
96
+ # @param key [String]
97
+ # @return [Array<String>]
98
+ #
99
+ def param_values key
100
+ key = key.downcase
101
+ @params.inject([]) { |a, (k, v)| key == k ? a << v : a }
102
+ end
103
+
104
+ ##
105
+ # The first value of the "charset" parameter, or nil if there is no
106
+ # charset.
107
+ # @return [String,nil]
108
+ #
109
+ def charset
110
+ param_values("charset").first
111
+ end
112
+
113
+ ## @private
114
+ def == other
115
+ other.is_a?(ContentType) && canonical_string == other.canonical_string
116
+ end
117
+ alias eql? ==
118
+
119
+ ## @private
120
+ def hash
121
+ canonical_string.hash
122
+ end
123
+
124
+ private
125
+
126
+ def initialize_params sections
127
+ params = sections.map do |s|
128
+ k, v = s.split "="
129
+ [k.strip.downcase, v.strip]
130
+ end
131
+ params.sort! do |(k1, v1), (k2, v2)|
132
+ a = k1 <=> k2
133
+ a.zero? ? v1 <=> v2 : a
134
+ end
135
+ params
136
+ end
137
+ end
138
+ end
139
+ end