google-serverless-exec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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