google-serverless-exec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 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
+ # http://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
+
18
+ require "shellwords"
19
+ require "English"
20
+
21
+ module Google
22
+ module Serverless
23
+ class Exec
24
+ ##
25
+ # A collection of utility functions and classes for interacting with an
26
+ # installation of the gcloud SDK.
27
+ #
28
+ module Gcloud
29
+ ##
30
+ # Base class for gcloud related errors.
31
+ #
32
+ class Error < ::StandardError
33
+ end
34
+
35
+ ##
36
+ # Exception raised when the gcloud binary could not be found.
37
+ #
38
+ class BinaryNotFound < Gcloud::Error
39
+ def initialize
40
+ super "GCloud binary not found in path"
41
+ end
42
+ end
43
+
44
+ ##
45
+ # Exception raised when the project gcloud config is not set.
46
+ #
47
+ class ProjectNotSet < Gcloud::Error
48
+ def initialize
49
+ super "GCloud project configuration not set"
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Exception raised when gcloud auth has not been completed.
55
+ #
56
+ class GcloudNotAuthenticated < Gcloud::Error
57
+ def initialize
58
+ super "GCloud not authenticated"
59
+ end
60
+ end
61
+
62
+ ##
63
+ # Exception raised when gcloud fails and returns an error.
64
+ #
65
+ class GcloudFailed < Gcloud::Error
66
+ def initialize code
67
+ super "GCloud failed with result code #{code}"
68
+ end
69
+ end
70
+
71
+ class << self
72
+ ##
73
+ # @private
74
+ # Returns the path to the gcloud binary, or nil if the binary could
75
+ # not be found.
76
+ #
77
+ # @return [String,nil] Path to the gcloud binary.
78
+ #
79
+ def binary_path
80
+ unless defined? @binary_path
81
+ @binary_path =
82
+ if ::Gem.win_platform?
83
+ `where gcloud` == "" ? nil : "gcloud"
84
+ else
85
+ path = `which gcloud`.strip
86
+ path.empty? ? nil : path
87
+ end
88
+ end
89
+ @binary_path
90
+ end
91
+
92
+ ##
93
+ # @private
94
+ # Returns the path to the gcloud binary. Raises BinaryNotFound if the
95
+ # binary could not be found.
96
+ #
97
+ # @return [String] Path to the gcloud binary.
98
+ # @raise [BinaryNotFound] The gcloud binary is not present.
99
+ #
100
+ def binary_path!
101
+ value = binary_path
102
+ raise BinaryNotFound unless value
103
+ value
104
+ end
105
+
106
+ ##
107
+ # @private
108
+ # Returns the ID of the current project, or nil if no project has
109
+ # been set.
110
+ #
111
+ # @return [String,nil] ID of the current project.
112
+ #
113
+ def current_project
114
+ unless defined? @current_project
115
+ params = [
116
+ "config", "list", "core/project", "--format=value(core.project)"
117
+ ]
118
+ @current_project = execute params, capture: true
119
+ @current_project = nil if @current_project.empty?
120
+ end
121
+ @current_project
122
+ end
123
+
124
+ ##
125
+ # @private
126
+ # Returns the ID of the current project. Raises ProjectNotSet if no
127
+ # project has been set in the gcloud configuration.
128
+ #
129
+ # @return [String] ID of the current project.
130
+ # @raise [ProjectNotSet] The project config has not been set.
131
+ #
132
+ def current_project!
133
+ value = current_project
134
+ raise ProjectNotSet if value.empty?
135
+ value
136
+ end
137
+
138
+ ##
139
+ # @private
140
+ # Verifies that all gcloud related dependencies are satisfied.
141
+ # Specifically, verifies that the gcloud binary is installed and
142
+ # authenticated, and a project has been set.
143
+ #
144
+ # @raise [BinaryNotFound] The gcloud binary is not present.
145
+ # @raise [ProjectNotSet] The project config has not been set.
146
+ # @raise [GcloudNotAuthenticated] Gcloud has not been authenticated.
147
+ #
148
+ def verify!
149
+ binary_path!
150
+ current_project!
151
+ auths = execute ["auth", "list", "--format=value(account)"],
152
+ capture: true
153
+ raise GcloudNotAuthenticated if auths.empty?
154
+ end
155
+
156
+ ##
157
+ # @private
158
+ # Execute a given gcloud command in a subshell.
159
+ #
160
+ # @param args [Array<String>] The gcloud args.
161
+ # @param echo [boolean] Whether to echo the command to the terminal.
162
+ # Defaults to false.
163
+ # @param capture [boolean] If true, return the output. If false, return
164
+ # a boolean indicating success or failure. Defaults to false.
165
+ # @param assert [boolean] If true, raise GcloudFailed on failure.
166
+ # Defaults to true.
167
+ # @return [String,Integer] Either the output or the success status,
168
+ # depending on the value of the `capture` parameter.
169
+ #
170
+ def execute args, echo: false, capture: false, assert: true
171
+ cmd_array = [binary_path!] + args
172
+ cmd =
173
+ if ::Gem.win_platform?
174
+ cmd_array.join " "
175
+ else
176
+ ::Shellwords.join cmd_array
177
+ end
178
+ puts cmd if echo
179
+ result = capture ? `#{cmd}` : system(cmd)
180
+ code = $CHILD_STATUS.exitstatus
181
+ raise GcloudFailed, code if assert && code != 0
182
+ result
183
+ end
184
+
185
+ ##
186
+ # @private
187
+ # Execute a given gcloud command in a subshell, and return the output
188
+ # as a string.
189
+ #
190
+ # @param args [Array<String>] The gcloud args.
191
+ # @return [String] The command output.
192
+ #
193
+ def capture args
194
+ execute args, capture: true
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,381 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 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
+ # http://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
+
18
+ require "shellwords"
19
+
20
+ require "google/serverless/exec/gcloud"
21
+ require "google/serverless/exec"
22
+
23
+ module Google
24
+ module Serverless
25
+ class Exec
26
+ ##
27
+ # # Serverless Rake Tasks.
28
+ #
29
+ # To make these tasks available, add the line `require "google/serverless/exec/tasks"`
30
+ # to your Rakefile. If your app uses Ruby on Rails, then the serverless gem
31
+ # provides a railtie that adds its tasks automatically, so you don't have
32
+ # to do anything beyond adding the gem to your Gemfile.
33
+ #
34
+ # The following tasks are defined:
35
+ #
36
+ # ## Rake serverless:exec
37
+ #
38
+ # Executes a given command in the context of a serverless application.
39
+ # See {Google::Serverless::Exec} for more information on this capability.
40
+ #
41
+ # The command to be run may either be provided as a rake argument, or as
42
+ # command line arguments, delimited by two dashes `--`. (The dashes are
43
+ # needed to separate your command from rake arguments and flags.)
44
+ # For example, to run a production database migration, you can run either of
45
+ # the following equivalent commands:
46
+ #
47
+ # bundle exec rake "serverless:exec[bundle exec bin/rails db:migrate]"
48
+ # bundle exec rake serverless:exec -- bundle exec bin/rails db:migrate
49
+ #
50
+ # To display usage instructions, provide two dashes but no command:
51
+ #
52
+ # bundle exec rake serverless:exec --
53
+ #
54
+ # ### Parameters
55
+ #
56
+ # You may customize the behavior of the serverless execution through a few
57
+ # enviroment variable parameters. These are set via the normal mechanism at
58
+ # the end of a rake command line. For example, to set GAE_CONFIG:
59
+ #
60
+ # bundle exec rake serverless:exec GAE_CONFIG=myservice.yaml -- bundle exec bin/rails db:migrate
61
+ #
62
+ # Be sure to set these parameters before the double dash. Any arguments
63
+ # following the double dash are interpreted as part of the command itself.
64
+ #
65
+ # The following environment variable parameters are supported:
66
+ #
67
+ # #### GAE_TIMEOUT
68
+ #
69
+ # Amount of time to wait before serverless:exec terminates the command.
70
+ # Expressed as a string formatted like: "2h15m10s". Default is "10m".
71
+ #
72
+ # #### GAE_PROJECT
73
+ #
74
+ # The ID of your Google Cloud project. If not specified, uses the current
75
+ # project from gcloud.
76
+ #
77
+ # #### GAE_CONFIG
78
+ #
79
+ # Path to the App Engine config file, used when your app has multiple
80
+ # services, or the config file is otherwise not called `./app.yaml`. The
81
+ # config file is used to determine the name of the App Engine service.
82
+ #
83
+ # #### GAE_SERVICE
84
+ #
85
+ # Name of the service to be used. Overrides any service name specified in
86
+ # your config file.
87
+ #
88
+ # #### GAE_EXEC_STRATEGY
89
+ #
90
+ # The execution strategy to use. Valid values are "deployment" (which is the
91
+ # default for App Engine Standard apps) and "cloud_build" (which is the
92
+ # default for App Engine Flexible and Cloud Run apps).
93
+ #
94
+ # Normally you should leave the strategy set to the default. The main reason
95
+ # to change it is if your app runs on the Flexible Environment and talks to
96
+ # a database over a VPC (using a private IP address). The "cloud_build"
97
+ # strategy used by default for Flexible apps cannot connect to a VPC, so you
98
+ # should use "deployment" in this case. (But note that, otherwise, the
99
+ # "deployment" strategy is significantly slower for apps on the Flexible
100
+ # environment.)
101
+ #
102
+ # #### GAE_VERSION
103
+ #
104
+ # The version of the service, used to identify which application image to
105
+ # use to run your command. If not specified, uses the most recently created
106
+ # version, regardless of whether that version is actually serving traffic.
107
+ # Applies only to the "cloud_build" strategy. (The "deployment" strategy
108
+ # deploys its own temporary version of your app.)
109
+ #
110
+ # #### GAE_EXEC_WRAPPER_IMAGE
111
+ #
112
+ # The fully-qualified name of the wrapper image to use. (This is a Docker
113
+ # image that emulates the App Engine environment in Google Cloud Build for
114
+ # the "cloud_build" strategy, and applies only to that strategy.) Normally,
115
+ # you should not override this unless you are testing a new wrapper.
116
+ #
117
+ # #### CLOUD_BUILD_GCS_LOG_DIR
118
+ #
119
+ # GCS bucket name of the cloud build log when GAE_STRATEGY is "cloud_build".
120
+ # (ex. "gs://BUCKET-NAME/FOLDER-NAME")
121
+ module Tasks
122
+ ## @private
123
+ PROJECT_ENV = "GAE_PROJECT"
124
+ ## @private
125
+ STRATEGY_ENV = "GAE_EXEC_STRATEGY"
126
+ ## @private
127
+ CONFIG_ENV = "GAE_CONFIG"
128
+ ## @private
129
+ SERVICE_ENV = "GAE_SERVICE"
130
+ ## @private
131
+ VERSION_ENV = "GAE_VERSION"
132
+ ## @private
133
+ TIMEOUT_ENV = "GAE_TIMEOUT"
134
+ ## @private
135
+ WRAPPER_IMAGE_ENV = "GAE_EXEC_WRAPPER_IMAGE"
136
+ ## @private
137
+ GCS_LOG_DIR = "CLOUD_BUILD_GCS_LOG_DIR"
138
+ ## @private
139
+ PRODUCT_ENV = "PRODUCT"
140
+
141
+ @defined = false
142
+
143
+ class << self
144
+ ##
145
+ # @private
146
+ # Define rake tasks.
147
+ #
148
+ def define
149
+ if @defined
150
+ puts "Serverless rake tasks already defined."
151
+ return
152
+ end
153
+ @defined = true
154
+
155
+ setup_exec_task
156
+ end
157
+
158
+ private
159
+
160
+ def setup_exec_task
161
+ ::Rake.application.last_description =
162
+ "Execute the given command in a Google serverless application."
163
+ ::Rake::Task.define_task "serverless:exec", [:cmd] do |_t, args|
164
+ verify_gcloud_and_report_errors
165
+ command = extract_command args[:cmd], ::ARGV
166
+ selected_product = extract_product ::ENV[PRODUCT_ENV]
167
+ app_exec = Exec.new command,
168
+ project: ::ENV[PROJECT_ENV],
169
+ service: ::ENV[SERVICE_ENV],
170
+ config_path: ::ENV[CONFIG_ENV],
171
+ version: ::ENV[VERSION_ENV],
172
+ timeout: ::ENV[TIMEOUT_ENV],
173
+ wrapper_image: ::ENV[WRAPPER_IMAGE_ENV],
174
+ strategy: ::ENV[STRATEGY_ENV],
175
+ gcs_log_dir: ::ENV[GCS_LOG_DIR],
176
+ product: selected_product
177
+ start_and_report_errors app_exec
178
+ exit
179
+ end
180
+ end
181
+
182
+ def extract_command cmd, argv
183
+ if cmd
184
+ ::Shellwords.split cmd
185
+ else
186
+ i = (argv.index { |a| a.to_s == "--" } || -1) + 1
187
+ if i.zero?
188
+ report_error <<~MESSAGE
189
+ No command provided for serverless:exec.
190
+ Did you remember to delimit it with two dashes? e.g.
191
+ bundle exec rake serverless:exec -- bundle exec ruby myscript.rb
192
+ For detailed usage instructions, provide two dashes but no command:
193
+ bundle exec rake serverless:exec --
194
+ MESSAGE
195
+ end
196
+ command = ::ARGV[i..-1]
197
+ if command.empty?
198
+ show_usage
199
+ exit
200
+ end
201
+ command
202
+ end
203
+ end
204
+
205
+ def extract_product product
206
+ if product
207
+ product = product.dup
208
+ product.downcase!
209
+
210
+ case product
211
+ when "app_engine"
212
+ APP_ENGINE
213
+ when "cloud_run"
214
+ CLOUD_RUN
215
+ end
216
+ else
217
+ nil
218
+ end
219
+ end
220
+
221
+ def show_usage
222
+ puts <<~USAGE
223
+ rake serverless:exec
224
+ This Rake task executes a given command in the context of a serverless
225
+ application. For more information,
226
+ on this capability, see the Google::Serverless::Exec documentation at
227
+ http://www.rubydoc.info/gems/appengine/AppEngine/Exec
228
+ The command to be run may either be provided as a rake argument, or as
229
+ command line arguments delimited by two dashes `--`. (The dashes are
230
+ needed to separate your command from rake arguments and flags.)
231
+ For example, to run a production database migration, you can run either
232
+ of the following equivalent commands:
233
+ bundle exec rake "serverless:exec[bundle exec bin/rails db:migrate]"
234
+ bundle exec rake serverless:exec -- bundle exec bin/rails db:migrate
235
+ To display these usage instructions, provide two dashes but no command:
236
+ bundle exec rake serverless:exec --
237
+ You may customize the behavior of the serverless execution through a few
238
+ enviroment variable parameters. These are set via the normal mechanism at
239
+ the end of a rake command line but before the double dash. For example, to
240
+ set GAE_CONFIG:
241
+ bundle exec rake serverless:exec GAE_CONFIG=myservice.yaml -- bundle exec bin/rails db:migrate
242
+ Be sure to set these parameters before the double dash. Any arguments
243
+ following the double dash are interpreted as part of the command itself.
244
+ The following environment variable parameters are supported:
245
+ GAE_TIMEOUT
246
+ Amount of time to wait before serverless:exec terminates the command.
247
+ Expressed as a string formatted like: "2h15m10s". Default is "10m".
248
+ GAE_PROJECT
249
+ The ID of your Google Cloud project. If not specified, uses the current
250
+ project from gcloud.
251
+ GAE_CONFIG
252
+ Path to the App Engine config file, used when your app has multiple
253
+ services, or the config file is otherwise not called `./app.yaml`. The
254
+ config file is used to determine the name of the App Engine service.
255
+ GAE_SERVICE
256
+ Name of the service to be used. Overrides any service name specified in
257
+ your config file.
258
+ GAE_EXEC_STRATEGY
259
+ The execution strategy to use. Valid values are "deployment" (which is the
260
+ default for App Engine Standard apps) and "cloud_build" (which is the
261
+ default for App Engine Flexible and Cloud Run apps).
262
+ Normally you should leave the strategy set to the default. The main reason
263
+ to change it is if your app runs on the Flexible Environment and talks to
264
+ a database over a VPC (using a private IP address). The "cloud_build"
265
+ strategy used by default for Flexible apps cannot connect to a VPC, so you
266
+ should use "deployment" in this case. (But note that, otherwise, the
267
+ "deployment" strategy is significantly slower for apps on the Flexible
268
+ environment.)
269
+ GAE_VERSION
270
+ The version of the service, used to identify which application image to
271
+ use to run your command. If not specified, uses the most recently created
272
+ version, regardless of whether that version is actually serving traffic.
273
+ Applies only to the "cloud_build" strategy. (The "deployment" strategy
274
+ deploys its own temporary version of your app.)
275
+ GAE_EXEC_WRAPPER_IMAGE
276
+ The fully-qualified name of the wrapper image to use. (This is a Docker
277
+ image that emulates the App Engine environment in Google Cloud Build for
278
+ the "cloud_build" strategy, and applies only to that strategy.) Normally,
279
+ you should not override this unless you are testing a new wrapper.
280
+ CLOUD_BUILD_GCS_LOG_DIR
281
+ GCS bucket name of the cloud build log when GAE_STRATEGY is "cloud_build".
282
+ (ex. "gs://BUCKET-NAME/FOLDER-NAME")
283
+ PRODUCT
284
+ The serverless product to use. Valid values are "app_engine" and "cloud_run".
285
+ If not specified, autodetects the serverless product to use.
286
+ This rake task is provided by the "serverless" gem. To make these tasks
287
+ available, add the following line to your Rakefile:
288
+ require "google/serverless/exec/tasks"
289
+ If your app uses Ruby on Rails, the gem provides a railtie that adds its
290
+ tasks automatically, so you don't have to do anything beyond adding the
291
+ gem to your Gemfile.
292
+ For more information or to report issues, visit the Github page:
293
+ https://github.com/GoogleCloudPlatform/google-serverless-exec
294
+ USAGE
295
+ end
296
+
297
+ def verify_gcloud_and_report_errors
298
+ Exec::Gcloud.verify!
299
+ rescue Exec::Gcloud::BinaryNotFound
300
+ report_error <<~MESSAGE
301
+ Could not find the `gcloud` binary in your system path.
302
+ This tool requires the Google Cloud SDK. To download and install it,
303
+ visit https://cloud.google.com/sdk/downloads
304
+ MESSAGE
305
+ rescue Exec::Gcloud::GcloudNotAuthenticated
306
+ report_error <<~MESSAGE
307
+ The gcloud authorization has not been completed. If you have not yet
308
+ initialized the Google Cloud SDK, we recommend running the `gcloud init`
309
+ command as described at https://cloud.google.com/sdk/docs/initializing
310
+ Alternately, you may log in directly by running `gcloud auth login`.
311
+ MESSAGE
312
+ rescue Exec::Gcloud::ProjectNotSet
313
+ report_error <<~MESSAGE
314
+ The gcloud project configuration has not been set. If you have not yet
315
+ initialized the Google Cloud SDK, we recommend running the `gcloud init`
316
+ command as described at https://cloud.google.com/sdk/docs/initializing
317
+ Alternately, you may set the default project configuration directly by
318
+ running `gcloud config set project <project-name>`.
319
+ MESSAGE
320
+ end
321
+
322
+ def start_and_report_errors app_exec
323
+ app_exec.start
324
+ rescue Exec::ConfigFileNotFound => e
325
+ report_error <<~MESSAGE
326
+ Could not determine which service should run this command because the App
327
+ Engine config file "#{e.config_path}" was not found.
328
+ Specify the config file using the GAE_CONFIG argument. e.g.
329
+ bundle exec rake serverless:exec GAE_CONFIG=myapp.yaml -- myscript.sh
330
+ Alternately, you may specify a service name directly with GAE_SERVICE. e.g.
331
+ bundle exec rake serverless:exec GAE_SERVICE=myservice -- myscript.sh
332
+ MESSAGE
333
+ rescue Exec::BadConfigFileFormat => e
334
+ report_error <<~MESSAGE
335
+ Could not determine which service should run this command because the App
336
+ Engine config file "#{e.config_path}" was malformed.
337
+ It must be a valid YAML file.
338
+ Specify the config file using the GAE_CONFIG argument. e.g.
339
+ bundle exec rake serverless:exec GAE_CONFIG=myapp.yaml -- myscript.sh
340
+ Alternately, you may specify a service name directly with GAE_SERVICE. e.g.
341
+ bundle exec rake serverless:exec GAE_SERVICE=myservice -- myscript.sh
342
+ MESSAGE
343
+ rescue Exec::NoSuchVersion => e
344
+ if e.version
345
+ report_error <<~MESSAGE
346
+ Could not find version "#{e.version}" of service "#{e.service}".
347
+ Please double-check the version exists. To use the most recent version by
348
+ default, omit the GAE_VERSION argument.
349
+ MESSAGE
350
+ else
351
+ report_error <<~MESSAGE
352
+ Could not find any versions of service "#{e.service}".
353
+ Please double-check that you have deployed this service. If you want to run
354
+ a command against a different service, you may provide a GAE_CONFIG argument
355
+ pointing to your App Engine config file, or a GAE_SERVICE argument to specify
356
+ a service directly.
357
+ MESSAGE
358
+ end
359
+ rescue Exec::NoDefaultProject
360
+ report_error <<~MESSAGE
361
+ Could not get the default project from gcloud.
362
+ Please either set the current project using
363
+ gcloud config set project my-project-id
364
+ or specify the project by setting the GAE_PROJECT argument. e.g.
365
+ bundle exec rake serverless:exec GAE_PROJECT=my-project-id -- myscript.sh
366
+ MESSAGE
367
+ rescue Exec::UsageError => e
368
+ report_error e.message
369
+ end
370
+
371
+ def report_error str
372
+ ::STDERR.puts str
373
+ exit 1
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ ::Google::Serverless::Exec::Tasks.define