appengine 0.6.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8985bf9ca824b3567577328e9c844eabcab5691907b0eb8a3385c181c91d2f13
4
- data.tar.gz: 04d8e4d73ee85d186798b69748191ec14423aecabfa1cbc5446d6e79e29b438c
3
+ metadata.gz: 316bcfd3b1413ddbb299da04099876fc58f2b38f91bef06d6f9d7d23de0b2a7d
4
+ data.tar.gz: 787086e724920361bf91caa2a8320a84d5b9e794c15175bb70dec0ab1337d30d
5
5
  SHA512:
6
- metadata.gz: fa651f55f75abf3d21813891a34c26d9c27ffe0656f7b77e54f440a2bb8d747f751317327cdc250a72fc3e286e4a76b79ac4f44ace1f70f01d8d46d3a27866ec
7
- data.tar.gz: a03743e4a99da424fb1c588f83767389c36b82ddcb5271cba0462a5c7433bd113d3db1b668159dad5520eaa7fcb11ffcb88694133a0f46feb557ac69ada0bab2
6
+ metadata.gz: 81bc955696cda3b5042fa75b3c1e14b99724721a49e262fd178e9ec0c6c99442ab3c7598f484cb289a651f93eb4dfd61e6b811a4857a41e6da8d56290f7f4071
7
+ data.tar.gz: 52fe0dd05a899f5f9c09929e5dcf245cc4a64cbdd72b8a1e9adee0087928efe1d11b14379a1bdab549c47b025ddae9cab62cb383fc4bfa343321b5d891d8a53a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  This is the change history for the appengine gem.
4
4
 
5
+ ### 0.7.0 (2022-04-22)
6
+
7
+ #### Features
8
+
9
+ * add gcs log dir option
10
+ * Replace the appengine exec class with an alias to serverless-exec
11
+ * Update stackdriver dependency
12
+ #### Bug Fixes
13
+
14
+ * Fix exception when a shell command rather than a command array is given in Exec
15
+ * Fix failure in the appengine:exec cloud_build strategy when App Engine doesn't provide the image
16
+
5
17
  ## v0.6.0 (2020-12-02)
6
18
 
7
19
  * Fix failure in the appengine:exec cloud_build strategy when App Engine doesn't provide the image
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  Google App Engine Integration Tools
2
2
  ===================================
3
3
 
4
- [![CircleCI](https://circleci.com/gh/GoogleCloudPlatform/appengine-ruby.svg?style=svg)](https://circleci.com/gh/GoogleCloudPlatform/appengine-ruby)
5
4
  [![Gem Version](https://badge.fury.io/rb/appengine.svg)](https://badge.fury.io/rb/appengine)
6
5
 
7
6
  This repository contains the "appengine" gem, a collection of libraries and
@@ -15,843 +15,15 @@
15
15
  # limitations under the License.
16
16
 
17
17
 
18
- require "date"
19
- require "erb"
20
- require "json"
21
- require "net/http"
22
- require "securerandom"
23
- require "shellwords"
24
- require "tempfile"
25
- require "yaml"
26
-
27
- require "appengine/util/gcloud"
18
+ require "google/serverless/exec"
28
19
 
29
20
  module AppEngine
30
- ##
31
- # # App Engine remote execution
32
- #
33
- # This class provides a client for App Engine remote execution, allowing
34
- # App Engine applications to perform on-demand tasks in the App Engine
35
- # environment. This may be used for safe running of ops and maintenance
36
- # tasks, such as database migrations, that access production cloud resources.
37
- #
38
- # ## About App Engine execution
39
- #
40
- # App Engine execution spins up a one-off copy of an App Engine app, and runs
41
- # a command against it. For example, if your app runs on Ruby on Rails, then
42
- # you might use App Engine execution to run a command such as
43
- # `bundle exec bin/rails db:migrate` in production infrastructure (to avoid
44
- # having to connect directly to your production database from a local
45
- # workstation).
46
- #
47
- # App Engine execution provides two strategies for generating that "one-off
48
- # copy":
49
- #
50
- # * A `deployment` strategy, which deploys a temporary version of your app
51
- # to a single backend instance and runs the command there.
52
- # * A `cloud_build` strategy, which deploys your application image to
53
- # Google Cloud Build and runs the command there.
54
- #
55
- # Both strategies are generally designed to emulate the App Engine runtime
56
- # environment on cloud VMs similar to those used by actual deployments of
57
- # your app. Both provide your application code and environment variables, and
58
- # both provide access to Cloud SQL connections used by your app. However,
59
- # they differ in what *version* of your app code they run against, and in
60
- # certain other constraints and performance characteristics. More detailed
61
- # information on using the two strategies is provided in the sections below.
62
- #
63
- # Apps deployed to the App Engine *flexible environment* will use the
64
- # `cloud_build` strategy by default. However, you can force an app to use the
65
- # `deployment` strategy instead. (You might do so if you need to connect to a
66
- # Cloud SQL database on a VPC using a private IP, because the `cloud_build`
67
- # strategy does not support private IPs.) To force use of `deployment`, set
68
- # the `strategy` parameter in the {AppEngine::Exec} constructor (or the
69
- # corresponding `GAE_EXEC_STRATEGY` parameter in the Rake task). Note that
70
- # the `deployment` strategy is usually significantly slower than
71
- # `cloud_build` for apps in the flexible environment.
72
- #
73
- # Apps deployed to the App Engine *standard environment* will *always* use
74
- # the `deployment` strategy. You cannot force use of the `cloud_build`
75
- # strategy.
76
- #
77
- # ## Prerequisites
78
- #
79
- # To use App Engine remote execution, you will need:
80
- #
81
- # * An app deployed to Google App Engine, of course!
82
- # * The [gcloud SDK](https://cloud.google.com/sdk/) installed and configured.
83
- # * The `appengine` gem.
84
- #
85
- # You may use the `AppEngine::Exec` class to run commands directly. However,
86
- # in most cases, it will be easier to run commands via the provided rake
87
- # tasks (see {AppEngine::Tasks}).
88
- #
89
- # ## Using the "deployment" strategy
90
- #
91
- # The `deployment` strategy deploys a temporary version of your app to a
92
- # single backend App Engine instance, runs the command there, and then
93
- # deletes the temporary version when it is finished.
94
- #
95
- # This is the default strategy (and indeed the only option) for apps running
96
- # on the App Engine standard environment. It can also be used for flexible
97
- # environment apps, but this is not commonly done because deployment of
98
- # flexible environment apps can take a long time.
99
- #
100
- # Because the `deployment` strategy deploys a temporary version of your app,
101
- # it runs against the *current application code* present where the command
102
- # was initiated (i.e. the code currently on your workstation if you run the
103
- # rake task from your workstation, or the current code on the branch if you
104
- # are running from a CI/CD system.) This may be different from the code
105
- # actually running in production, so it is important that you run from a
106
- # compatible code branch.
107
- #
108
- # ### Specifying the host application
109
- #
110
- # The `deployment` strategy works by deploying a temporary version of your
111
- # app, so that it has access to your app's project and settings in App
112
- # Engine. In most cases, it can determine this automatically, but depending
113
- # on how your app or environment is structured, you may need to give it some
114
- # help.
115
- #
116
- # By default, your Google Cloud project is taken from the current gcloud
117
- # project. If you need to override this, set the `:project` parameter in the
118
- # {AppEngine::Exec} constructor (or the corresponding `GAE_PROJECT`
119
- # parameter in the Rake task).
120
- #
121
- # By default, the service name is taken from the App Engine config file.
122
- # App Engine execution will assume this file is called `app.yaml` in the
123
- # current directory. To use a different config file, set the `config_path`
124
- # parameter in the {AppEngine::Exec} constructor (or the corresponding
125
- # `GAE_CONFIG` parameter in the Rake task). You may also set the service name
126
- # directly, using the `service` parameter (or `GAE_SERVICE` in Rake).
127
- #
128
- # ### Providing credentials
129
- #
130
- # Your command will effectively be a deployment of your App Engine app
131
- # itself, and will have access to the same credentials. For example, App
132
- # Engine provides a service account by default for your app, or your app may
133
- # be making use of its own service account key. In either case, make sure the
134
- # service account has sufficient access for the command you want to run
135
- # (such as database admin credentials).
136
- #
137
- # ### Other options
138
- #
139
- # You may also provide a timeout, which is the length of time that App
140
- # Engine execution will allow your command to run before it is considered to
141
- # have stalled and is terminated. The timeout should be a string of the form
142
- # `2h15m10s`. The default is `10m`.
143
- #
144
- # The timeout is set via the `timeout` parameter to the {AppEngine::Exec}
145
- # constructor, or by setting the `GAE_TIMEOUT` environment variable when
146
- # invoking using Rake.
147
- #
148
- # ### Resource usage and billing
149
- #
150
- # The `deployment` strategy deploys to a temporary instance of your app in
151
- # order to run the command. You may be billed for that usage. However, the
152
- # cost should be minimal, because it will then immediately delete that
153
- # instance in order to minimize usage.
154
- #
155
- # If you interrupt the execution (or it crashes), it is possible that the
156
- # temporary instance may not get deleted properly. If you suspect this may
157
- # have happened, go to the App Engine tab in the cloud console, under
158
- # "versions" of your service, and delete the temporary version manually. It
159
- # will have a name matching the pattern `appengine-exec-<timestamp>`.
160
- #
161
- # ## Using the "cloud_build" strategy
162
- #
163
- # The `cloud_build` strategy takes the application image that App Engine is
164
- # actually using to run your app, and uses it to spin up a copy of your app
165
- # in [Google Cloud Build](https://cloud.google.com/cloud-build) (along with
166
- # an emulation layer that emulates certain App Engine services such as Cloud
167
- # SQL connection sockets). The command then gets run in the Cloud Build
168
- # environment.
169
- #
170
- # This is the default strategy for apps running on the App Engine flexible
171
- # environment. (It is not available for standard environment apps.) Note that
172
- # the `cloud_build` strategy cannot be used if your command needs to connect
173
- # to a database over a [VPC](https://cloud.google.com/vpc/) private IP
174
- # address. This is because it runs on virtual machines provided by the Cloud
175
- # Build service, which are not part of your VPC. If your database can be
176
- # accessed only over a private IP, you should use the `deployment` strategy
177
- # instead.
178
- #
179
- # The Cloud Build log is output to the directory specified by
180
- # "CLOUD_BUILD_GCS_LOG_DIR". (ex. "gs://BUCKET-NAME/FOLDER-NAME")
181
- # By default, log directory name is
182
- # "gs://[PROJECT_NUMBER].cloudbuild-logs.googleusercontent.com/".
183
- #
184
- # ### Specifying the host application
185
- #
186
- # The `cloud_build` strategy needs to know exactly which app, service, and
187
- # version of your app, to identify the application image to use.
188
- #
189
- # By default, your Google Cloud project is taken from the current gcloud
190
- # project. If you need to override this, set the `:project` parameter in the
191
- # {AppEngine::Exec} constructor (or the corresponding `GAE_PROJECT`
192
- # parameter in the Rake task).
193
- #
194
- # By default, the service name is taken from the App Engine config file.
195
- # App Engine execution will assume this file is called `app.yaml` in the
196
- # current directory. To use a different config file, set the `config_path`
197
- # parameter in the {AppEngine::Exec} constructor (or the corresponding
198
- # `GAE_CONFIG` parameter in the Rake task). You may also set the service name
199
- # directly, using the `service` parameter (or `GAE_SERVICE` in Rake).
200
- #
201
- # By default, the image of the most recently deployed version of your app is
202
- # used. (Note that this most recently deployed version may not be the same
203
- # version that is currently receiving traffic: for example, if you deployed
204
- # with `--no-promote`.) To use a different version, set the `version`
205
- # parameter in the {AppEngine::Exec} constructor (or the corresponding
206
- # `GAE_VERSION` parameter in the Rake task).
207
- #
208
- # ### Providing credentials
209
- #
210
- # By default, the `cloud_build` strategy uses your project's Cloud Build
211
- # service account for its credentials. Unless your command provides its own
212
- # service account key, you may need to grant the Cloud Build service account
213
- # any permissions needed to execute your command (such as access to your
214
- # database). For most tasks, it is sufficient to grant Project Editor
215
- # permissions to the service account. You can find the service account
216
- # configuration in the IAM tab in the Cloud Console under the name
217
- # `[your-project-number]@cloudbuild.gserviceaccount.com`.
218
- #
219
- # ### Other options
220
- #
221
- # You may also provide a timeout, which is the length of time that App
222
- # Engine execution will allow your command to run before it is considered to
223
- # have stalled and is terminated. The timeout should be a string of the form
224
- # `2h15m10s`. The default is `10m`.
225
- #
226
- # The timeout is set via the `timeout` parameter to the {AppEngine::Exec}
227
- # constructor, or by setting the `GAE_TIMEOUT` environment variable when
228
- # invoking using Rake.
229
- #
230
- # You can also set the wrapper image used to emulate the App Engine runtime
231
- # environment, by setting the `wrapper_image` parameter to the constructor,
232
- # or by setting the `GAE_EXEC_WRAPPER_IMAGE` environment variable. Generally,
233
- # you will not need to do this unless you are testing a new wrapper image.
234
- #
235
- # ### Resource usage and billing
236
- #
237
- # The `cloud_build` strategy uses virtual machine resources provided by
238
- # Google Cloud Build. Generally, a certain number of usage minutes per day is
239
- # covered under a free tier, but additional compute usage beyond that time is
240
- # billed to your Google Cloud account. For more details,
241
- # see https://cloud.google.com/cloud-build/pricing
242
- #
243
- # If your command makes API calls or utilizes other cloud resources, you may
244
- # also be billed for that usage. However, the `cloud_build` strategy (unlike
245
- # the `deployment` strategy) does not use actual App Engine instances, and
246
- # you will not be billed for additional App Engine instance usage.
247
21
  #
248
- class Exec
249
- @default_timeout = "10m"
250
- @default_service = "default"
251
- @default_config_path = "./app.yaml"
252
- @default_wrapper_image = "gcr.io/google-appengine/exec-wrapper:latest"
253
-
254
- ##
255
- # Base class for exec-related usage errors.
256
- #
257
- class UsageError < ::StandardError
258
- end
259
-
260
- ##
261
- # Unsupported strategy
262
- #
263
- class UnsupportedStrategy < UsageError
264
- def initialize strategy, app_env
265
- @strategy = strategy
266
- @app_env = app_env
267
- super "Strategy \"#{strategy}\" not supported for the #{app_env}" \
268
- " environment"
269
- end
270
- attr_reader :strategy
271
- attr_reader :app_env
272
- end
273
-
274
- ##
275
- # Exception raised when a parameter is malformed.
276
- #
277
- class BadParameter < UsageError
278
- def initialize param, value
279
- @param_name = param
280
- @value = value
281
- super "Bad value for #{param}: #{value}"
282
- end
283
- attr_reader :param_name
284
- attr_reader :value
285
- end
286
-
287
- ##
288
- # Exception raised when gcloud has no default project.
289
- #
290
- class NoDefaultProject < UsageError
291
- def initialize
292
- super "No default project set."
293
- end
294
- end
295
-
296
- ##
297
- # Exception raised when the App Engine config file could not be found.
298
- #
299
- class ConfigFileNotFound < UsageError
300
- def initialize config_path
301
- @config_path = config_path
302
- super "Config file #{config_path} not found."
303
- end
304
- attr_reader :config_path
305
- end
306
-
307
- ##
308
- # Exception raised when the App Engine config file could not be parsed.
309
- #
310
- class BadConfigFileFormat < UsageError
311
- def initialize config_path
312
- @config_path = config_path
313
- super "Config file #{config_path} malformed."
314
- end
315
- attr_reader :config_path
316
- end
317
-
318
- ##
319
- # Exception raised when the given version could not be found, or no
320
- # versions at all could be found for the given service.
321
- #
322
- class NoSuchVersion < UsageError
323
- def initialize service, version = nil
324
- @service = service
325
- @version = version
326
- if version
327
- super "No such version \"#{version}\" for service \"#{service}\""
328
- else
329
- super "No versions found for service \"#{service}\""
330
- end
331
- end
332
- attr_reader :service
333
- attr_reader :version
334
- end
335
-
336
- class << self
337
- ## @return [String] Default command timeout.
338
- attr_accessor :default_timeout
339
-
340
- ## @return [String] Default service name if the config doesn't specify.
341
- attr_accessor :default_service
342
-
343
- ## @return [String] Path to default config file.
344
- attr_accessor :default_config_path
345
-
346
- ## @return [String] Docker image that implements the app engine wrapper.
347
- attr_accessor :default_wrapper_image
348
-
349
- ##
350
- # Create an execution for a rake task.
351
- #
352
- # @param name [String] Name of the task
353
- # @param args [Array<String>] Args to pass to the task
354
- # @param env_args [Array<String>] Environment variable settings, each
355
- # of the form `NAME=value`.
356
- # @param service [String,nil] Name of the service. If omitted, obtains
357
- # the service name from the config file.
358
- # @param config_path [String,nil] App Engine config file to get the
359
- # service name from if the service name is not provided directly.
360
- # If omitted, defaults to the value returned by
361
- # {AppEngine::Exec.default_config_path}.
362
- # @param version [String,nil] Version string. If omitted, defaults to the
363
- # most recently created version of the given service (which may not
364
- # be the one currently receiving traffic).
365
- # @param timeout [String,nil] Timeout string. If omitted, defaults to the
366
- # value returned by {AppEngine::Exec.default_timeout}.
367
- # @param wrapper_image [String,nil] The fully qualified name of the
368
- # wrapper image to use. (Applies only to the "cloud_build" strategy.)
369
- # @param strategy [String,nil] The execution strategy to use, or `nil` to
370
- # choose a default based on the App Engine environment (flexible or
371
- # standard). Allowed values are `nil`, `"deployment"` (which is the
372
- # default for Standard), and `"cloud_build"` (which is the default
373
- # for Flexible).
374
- # @param gcs_log_dir [String,nil] GCS bucket name of the cloud build log
375
- # when strategy is "cloud_build". (ex. "gs://BUCKET-NAME/FOLDER-NAME")
376
- #
377
- def new_rake_task name, args: [], env_args: [],
378
- service: nil, config_path: nil, version: nil,
379
- timeout: nil, project: nil, wrapper_image: nil,
380
- strategy: nil, gcs_log_dir: nil
381
- escaped_args = args.map do |arg|
382
- arg.gsub(/[,\[\]]/) { |m| "\\#{m}" }
383
- end
384
- name_with_args =
385
- if escaped_args.empty?
386
- name
387
- else
388
- "#{name}[#{escaped_args.join ','}]"
389
- end
390
- new ["bundle", "exec", "rake", name_with_args] + env_args,
391
- service: service, config_path: config_path, version: version,
392
- timeout: timeout, project: project, wrapper_image: wrapper_image,
393
- strategy: strategy, gcs_log_dir: gcs_log_dir
394
- end
395
- end
396
-
397
- ##
398
- # Create an execution for the given command.
399
- #
400
- # @param command [Array<String>] The command in array form.
401
- # @param project [String,nil] ID of the project. If omitted, obtains
402
- # the project from gcloud.
403
- # @param service [String,nil] Name of the service. If omitted, obtains
404
- # the service name from the config file.
405
- # @param config_path [String,nil] App Engine config file to get the
406
- # service name from if the service name is not provided directly.
407
- # If omitted, defaults to the value returned by
408
- # {AppEngine::Exec.default_config_path}.
409
- # @param version [String,nil] Version string. If omitted, defaults to the
410
- # most recently created version of the given service (which may not be
411
- # the one currently receiving traffic).
412
- # @param timeout [String,nil] Timeout string. If omitted, defaults to the
413
- # value returned by {AppEngine::Exec.default_timeout}.
414
- # @param wrapper_image [String,nil] The fully qualified name of the wrapper
415
- # image to use. (Applies only to the "cloud_build" strategy.)
416
- # @param strategy [String,nil] The execution strategy to use, or `nil` to
417
- # choose a default based on the App Engine environment (flexible or
418
- # standard). Allowed values are `nil`, `"deployment"` (which is the
419
- # default for Standard), and `"cloud_build"` (which is the default for
420
- # Flexible).
421
- # @param gcs_log_dir [String,nil] GCS bucket name of the cloud build log
422
- # when strategy is "cloud_build". (ex. "gs://BUCKET-NAME/FOLDER-NAME")
423
- #
424
- def initialize command,
425
- project: nil, service: nil, config_path: nil, version: nil,
426
- timeout: nil, wrapper_image: nil, strategy: nil, gcs_log_dir: nil
427
- @command = command
428
- @service = service
429
- @config_path = config_path
430
- @version = version
431
- @timeout = timeout
432
- @project = project
433
- @wrapper_image = wrapper_image
434
- @strategy = strategy
435
- @gcs_log_dir = gcs_log_dir
436
-
437
- yield self if block_given?
438
- end
439
-
440
- ##
441
- # @return [String] The project ID.
442
- # @return [nil] if the default gcloud project should be used.
443
- #
444
- attr_accessor :project
445
-
446
- ##
447
- # @return [String] The service name.
448
- # @return [nil] if the service should be obtained from the app config.
449
- #
450
- attr_accessor :service
451
-
452
- ##
453
- # @return [String] Path to the config file.
454
- # @return [nil] if the default of `./app.yaml` should be used.
455
- #
456
- attr_accessor :config_path
457
-
458
- ##
459
- # @return [String] Service version of the image to use.
460
- # @return [nil] if the most recent should be used.
461
- #
462
- attr_accessor :version
463
-
464
- ##
465
- # @return [String] The command timeout, in `1h23m45s` format.
466
- # @return [nil] if the default of `10m` should be used.
467
- #
468
- attr_accessor :timeout
469
-
470
- ##
471
- # The command to run.
472
- #
473
- # @return [String] if the command is a script to be run in a shell.
474
- # @return [Array<String>] if the command is a posix command to be run
475
- # directly without a shell.
476
- #
477
- attr_accessor :command
478
-
479
- ##
480
- # @return [String] Custom wrapper image to use.
481
- # @return [nil] if the default should be used.
482
- #
483
- attr_accessor :wrapper_image
484
-
485
- ##
486
- # @return [String] The execution strategy to use. Allowed values are
487
- # `"deployment"` and `"cloud_build"`.
488
- # @return [nil] to choose a default based on the App Engine environment
489
- # (flexible or standard).
490
- #
491
- attr_accessor :strategy
492
-
493
- ##
494
- # Executes the command synchronously. Streams the logs back to standard out
495
- # and does not return until the command has completed or timed out.
496
- #
497
- def start
498
- resolve_parameters
499
- app_info = version_info @service, @version
500
- resolve_strategy app_info["env"]
501
- if @strategy == "cloud_build"
502
- start_build_strategy app_info
503
- else
504
- start_deployment_strategy app_info
505
- end
506
- end
507
-
508
- private
509
-
510
- ##
511
- # @private
512
- # Resolves and canonicalizes all the parameters.
513
- #
514
- def resolve_parameters
515
- @timestamp_suffix = ::Time.now.strftime "%Y%m%d%H%M%S"
516
- @command = ::Shellwords.split @command.to_s unless @command.is_a? Array
517
- @project ||= default_project
518
- @service ||= service_from_config || Exec.default_service
519
- @version ||= latest_version @service
520
- @timeout ||= Exec.default_timeout
521
- @timeout_seconds = parse_timeout @timeout
522
- @wrapper_image ||= Exec.default_wrapper_image
523
- self
524
- end
525
-
526
- def resolve_strategy app_env
527
- @strategy = @strategy.to_s.downcase
528
- if @strategy.empty?
529
- @strategy = app_env == "flexible" ? "cloud_build" : "deployment"
530
- end
531
- if app_env == "standard" && @strategy == "cloud_build" ||
532
- @strategy != "cloud_build" && @strategy != "deployment"
533
- raise UnsupportedStrategy.new @strategy, app_env
534
- end
535
- @strategy
536
- end
537
-
538
- def service_from_config
539
- return nil if !@config_path && @service
540
- @config_path ||= Exec.default_config_path
541
- ::YAML.load_file(config_path)["service"]
542
- rescue ::Errno::ENOENT
543
- raise ConfigFileNotFound, @config_path
544
- rescue ::StandardError
545
- raise BadConfigFileFormat, @config_path
546
- end
547
-
548
- def default_project
549
- result = Util::Gcloud.execute \
550
- ["config", "get-value", "project"],
551
- capture: true, assert: false
552
- result.strip!
553
- raise NoDefaultProject if result.empty?
554
- result
555
- end
556
-
557
- def parse_timeout timeout_str
558
- matched = timeout_str =~ /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/
559
- raise BadParameter.new "timeout", timeout_str unless matched
560
- hours = ::Regexp.last_match(1).to_i
561
- minutes = ::Regexp.last_match(2).to_i
562
- seconds = ::Regexp.last_match(3).to_i
563
- hours * 3600 + minutes * 60 + seconds
564
- end
565
-
566
- ##
567
- # @private
568
- # Returns the name of the most recently created version of the given
569
- # service.
570
- #
571
- # @param service [String] Name of the service.
572
- # @return [String] Name of the most recent version.
573
- #
574
- def latest_version service
575
- result = Util::Gcloud.execute \
576
- [
577
- "app", "versions", "list",
578
- "--project", @project,
579
- "--service", service,
580
- "--format", "get(version.id)",
581
- "--sort-by", "~version.createTime",
582
- "--limit", "1"
583
- ],
584
- capture: true, assert: false
585
- result = result.split.first
586
- raise NoSuchVersion, service unless result
587
- result
588
- end
589
-
590
- ##
591
- # @private
592
- # Returns full information on the given version of the given service.
593
- #
594
- # @param service [String] Name of the service. If omitted, the service
595
- # "default" is used.
596
- # @param version [String] Name of the version. If omitted, the most
597
- # recently deployed is used.
598
- # @return [Hash] A collection of fields parsed from the JSON representation
599
- # of the version
600
- # @return [nil] if the requested version doesn't exist.
601
- #
602
- def version_info service, version
603
- service ||= "default"
604
- version ||= latest_version service
605
- result = Util::Gcloud.execute \
606
- [
607
- "app", "versions", "describe", version,
608
- "--project", @project,
609
- "--service", service,
610
- "--format", "json"
611
- ],
612
- capture: true, assert: false
613
- result.strip!
614
- raise NoSuchVersion.new(service, version) if result.empty?
615
- ::JSON.parse result
616
- end
617
-
618
- ##
619
- # @private
620
- # Performs exec on a GAE standard app.
621
- #
622
- def start_deployment_strategy app_info
623
- describe_deployment_strategy
624
- entrypoint_file = app_yaml_file = temp_version = nil
625
- begin
626
- puts "\n---------- DEPLOY COMMAND ----------"
627
- secret = create_secret
628
- entrypoint_file = copy_entrypoint secret
629
- app_yaml_file = copy_app_yaml app_info, entrypoint_file
630
- temp_version = deploy_temp_app app_yaml_file
631
- puts "\n---------- EXECUTE COMMAND ----------"
632
- puts "COMMAND: #{@command.inspect}\n\n"
633
- exit_status = track_status temp_version, secret
634
- puts "\nEXIT STATUS: #{exit_status}"
635
- ensure
636
- puts "\n---------- CLEANUP ----------"
637
- ::File.unlink entrypoint_file if entrypoint_file
638
- ::File.unlink app_yaml_file if app_yaml_file
639
- delete_temp_version temp_version
640
- end
641
- end
642
-
643
- def describe_deployment_strategy
644
- puts "\nUsing the `deployment` strategy for appengine:exec"
645
- puts "(i.e. deploying a temporary version of your app)"
646
- puts "PROJECT: #{@project}"
647
- puts "SERVICE: #{@service}"
648
- puts "TIMEOUT: #{@timeout}"
649
- end
650
-
651
- def create_secret
652
- ::SecureRandom.alphanumeric 20
653
- end
654
-
655
- def copy_entrypoint secret
656
- entrypoint_template =
657
- ::File.join(::File.dirname(::File.dirname(__dir__)),
658
- "data", "exec_standard_entrypoint.rb.erb")
659
- entrypoint_file = "appengine_exec_entrypoint_#{@timestamp_suffix}.rb"
660
- erb = ::ERB.new ::File.read entrypoint_template
661
- data = {
662
- secret: secret.inspect, command: command.inspect
663
- }
664
- result = erb.result_with_hash data
665
- ::File.open entrypoint_file, "w" do |file|
666
- file.write result
667
- end
668
- entrypoint_file
669
- end
670
-
671
- def copy_app_yaml app_info, entrypoint_file
672
- yaml_data = {
673
- "runtime" => app_info["runtime"],
674
- "service" => @service,
675
- "entrypoint" => "ruby #{entrypoint_file}",
676
- "env_variables" => app_info["envVariables"],
677
- "manual_scaling" => { "instances" => 1 }
678
- }
679
- if app_info["env"] == "flexible"
680
- complete_flex_app_yaml yaml_data, app_info
681
- else
682
- complete_standard_app_yaml yaml_data, app_info
683
- end
684
- app_yaml_file = "appengine_exec_config_#{@timestamp_suffix}.yaml"
685
- ::File.open app_yaml_file, "w" do |file|
686
- ::Psych.dump yaml_data, file
687
- end
688
- app_yaml_file
689
- end
690
-
691
- def complete_flex_app_yaml yaml_data, app_info
692
- yaml_data["env"] = "flex"
693
- orig_path = (app_info["betaSettings"] || {})["module_yaml_path"]
694
- return unless orig_path
695
- orig_yaml = ::YAML.load_file orig_path
696
- copy_keys = ["skip_files", "resources", "network", "runtime_config",
697
- "beta_settings"]
698
- copy_keys.each do |key|
699
- yaml_data[key] = orig_yaml[key] if orig_yaml[key]
700
- end
701
- end
702
-
703
- def complete_standard_app_yaml yaml_data, app_info
704
- yaml_data["instance_class"] = app_info["instanceClass"].sub(/^F/, "B")
705
- end
706
-
707
- def deploy_temp_app app_yaml_file
708
- temp_version = "appengine-exec-#{@timestamp_suffix}"
709
- Util::Gcloud.execute [
710
- "app", "deploy", app_yaml_file,
711
- "--project", @project,
712
- "--version", temp_version,
713
- "--no-promote", "--quiet"
714
- ]
715
- temp_version
716
- end
717
-
718
- def track_status temp_version, secret
719
- host = "#{temp_version}.#{@service}.#{@project}.appspot.com"
720
- ::Net::HTTP.start host do |http|
721
- outpos = errpos = 0
722
- delay = 0.0
723
- loop do
724
- sleep delay
725
- uri = URI("http://#{host}/#{secret}")
726
- uri.query = ::URI.encode_www_form outpos: outpos, errpos: errpos
727
- response = http.request_get uri
728
- data = ::JSON.parse response.body
729
- data["outlines"].each { |line| puts "[STDOUT] #{line}" }
730
- data["errlines"].each { |line| puts "[STDERR] #{line}" }
731
- outpos = data["outpos"]
732
- errpos = data["errpos"]
733
- return data["status"] if data["status"]
734
- if data["time"] > @timeout_seconds
735
- http.request_post "/#{secret}/kill", ""
736
- return "timeout"
737
- end
738
- if data["outlines"].empty? && data["errlines"].empty?
739
- delay += 0.1
740
- delay = 1.0 if delay > 1.0
741
- else
742
- delay = 0.0
743
- end
744
- end
745
- end
746
- end
747
-
748
- def delete_temp_version temp_version
749
- Util::Gcloud.execute [
750
- "app", "versions", "delete", temp_version,
751
- "--project", @project,
752
- "--service", @service,
753
- "--quiet"
754
- ]
755
- end
756
-
757
- ##
758
- # @private
759
- # Performs exec on a GAE flexible app.
760
- #
761
- def start_build_strategy app_info
762
- env_variables = app_info["envVariables"] || {}
763
- beta_settings = app_info["betaSettings"] || {}
764
- cloud_sql_instances = beta_settings["cloud_sql_instances"] || []
765
- container = app_info["deployment"]["container"]
766
- image = container ? container["image"] : image_from_build(app_info)
767
-
768
- describe_build_strategy
769
-
770
- config = build_config command, image, env_variables, cloud_sql_instances
771
- file = ::Tempfile.new ["cloudbuild_", ".json"]
772
- begin
773
- ::JSON.dump config, file
774
- file.flush
775
- execute_command = [
776
- "builds", "submit",
777
- "--project", @project,
778
- "--no-source",
779
- "--config", file.path,
780
- "--timeout", @timeout
781
- ]
782
- execute_command.concat ["--gcs-log-dir", @gcs_log_dir] unless @gcs_log_dir.nil?
783
- Util::Gcloud.execute execute_command
784
- ensure
785
- file.close!
786
- end
787
- end
788
-
789
- ##
790
- # @private
791
- # Workaround for https://github.com/GoogleCloudPlatform/appengine-ruby/issues/33
792
- # Determines the image by looking it up in Cloud Build
793
- #
794
- def image_from_build app_info
795
- create_time = ::DateTime.parse(app_info["createTime"]).to_time.utc
796
- after_time = (create_time - 3600).strftime "%Y-%m-%dT%H:%M:%SZ"
797
- before_time = (create_time + 3600).strftime "%Y-%m-%dT%H:%M:%SZ"
798
- partial_uri = "gcr.io/#{@project}/appengine/#{@service}.#{@version}"
799
- filter = "createTime>#{after_time} createTime<#{before_time} images[]:#{partial_uri}"
800
- result = Util::Gcloud.execute \
801
- [
802
- "builds", "list",
803
- "--project", @project,
804
- "--filter", filter,
805
- "--format", "json"
806
- ],
807
- capture: true, assert: false
808
- result.strip!
809
- raise NoSuchVersion.new(@service, @version) if result.empty?
810
- build_info = ::JSON.parse(result).first
811
- build_info["images"].first
812
- end
813
-
814
- def describe_build_strategy
815
- puts "\nUsing the `cloud_build` strategy for appengine:exec"
816
- puts "(i.e. running your app image in Cloud Build)"
817
- puts "PROJECT: #{@project}"
818
- puts "SERVICE: #{@service}"
819
- puts "VERSION: #{@version}"
820
- puts "TIMEOUT: #{@timeout}"
821
- puts ""
822
- end
823
-
824
- ##
825
- # @private
826
- # Builds a cloudbuild config as a data structure.
827
- #
828
- # @param command [Array<String>] The command in array form.
829
- # @param image [String] The fully qualified image path.
830
- # @param env_variables [Hash<String,String>] Environment variables.
831
- # @param cloud_sql_instances [String,Array<String>] Names of cloud sql
832
- # instances to connect to.
833
- #
834
- def build_config command, image, env_variables, cloud_sql_instances
835
- args = ["-i", image]
836
- env_variables.each do |k, v|
837
- v = v.gsub "$", "$$"
838
- args << "-e" << "#{k}=#{v}"
839
- end
840
- unless cloud_sql_instances.empty?
841
- cloud_sql_instances = Array(cloud_sql_instances)
842
- cloud_sql_instances.each do |sql|
843
- args << "-s" << sql
844
- end
845
- end
846
- args << "--"
847
- args += command
22
+ # The Appengine gem uses the Google Serverless gem for remote execution.
23
+ # This may be used for safe running of ops and maintenance tasks, such as
24
+ # database migrations in a production serverless environment.
25
+ # See [Google Serverless Exec](https://www.rubydoc.info/gems/google-serverless-exec)
26
+ # for more information on the usage documentation
848
27
 
849
- {
850
- "steps" => [
851
- "name" => @wrapper_image,
852
- "args" => args
853
- ]
854
- }
855
- end
856
- end
28
+ Exec = Google::Serverless::Exec
857
29
  end
@@ -375,7 +375,7 @@ module AppEngine
375
375
  end
376
376
 
377
377
  def report_error str
378
- ::STDERR.puts str
378
+ warn str
379
379
  exit 1
380
380
  end
381
381
  end
@@ -17,5 +17,5 @@
17
17
 
18
18
  module AppEngine
19
19
  # The current version of this gem, as a string.
20
- VERSION = "0.6.0"
20
+ VERSION = "0.7.0"
21
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appengine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-02 00:00:00.000000000 Z
11
+ date: 2022-04-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-cloud-env
@@ -25,25 +25,39 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.4'
27
27
  - !ruby/object:Gem::Dependency
28
- name: stackdriver
28
+ name: google-serverless-exec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '0.20'
34
31
  - - ">="
35
32
  - !ruby/object:Gem::Version
36
- version: 0.20.1
33
+ version: '0.1'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: 2.a
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0.1'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: 2.a
47
+ - !ruby/object:Gem::Dependency
48
+ name: stackdriver
49
+ requirement: !ruby/object:Gem::Requirement
40
50
  requirements:
41
51
  - - "~>"
42
52
  - !ruby/object:Gem::Version
43
- version: '0.20'
44
- - - ">="
53
+ version: '0.21'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
45
59
  - !ruby/object:Gem::Version
46
- version: 0.20.1
60
+ version: '0.21'
47
61
  - !ruby/object:Gem::Dependency
48
62
  name: bundler
49
63
  requirement: !ruby/object:Gem::Requirement
@@ -64,28 +78,28 @@ dependencies:
64
78
  requirements:
65
79
  - - "~>"
66
80
  - !ruby/object:Gem::Version
67
- version: 1.24.0
81
+ version: 1.25.1
68
82
  type: :development
69
83
  prerelease: false
70
84
  version_requirements: !ruby/object:Gem::Requirement
71
85
  requirements:
72
86
  - - "~>"
73
87
  - !ruby/object:Gem::Version
74
- version: 1.24.0
88
+ version: 1.25.1
75
89
  - !ruby/object:Gem::Dependency
76
90
  name: minitest
77
91
  requirement: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - "~>"
80
94
  - !ruby/object:Gem::Version
81
- version: '5.11'
95
+ version: '5.14'
82
96
  type: :development
83
97
  prerelease: false
84
98
  version_requirements: !ruby/object:Gem::Requirement
85
99
  requirements:
86
100
  - - "~>"
87
101
  - !ruby/object:Gem::Version
88
- version: '5.11'
102
+ version: '5.14'
89
103
  - !ruby/object:Gem::Dependency
90
104
  name: minitest-focus
91
105
  requirement: !ruby/object:Gem::Requirement
@@ -120,14 +134,14 @@ dependencies:
120
134
  requirements:
121
135
  - - "~>"
122
136
  - !ruby/object:Gem::Version
123
- version: '12.0'
137
+ version: '13.0'
124
138
  type: :development
125
139
  prerelease: false
126
140
  version_requirements: !ruby/object:Gem::Requirement
127
141
  requirements:
128
142
  - - "~>"
129
143
  - !ruby/object:Gem::Version
130
- version: '12.0'
144
+ version: '13.0'
131
145
  - !ruby/object:Gem::Dependency
132
146
  name: rdoc
133
147
  requirement: !ruby/object:Gem::Requirement
@@ -156,20 +170,6 @@ dependencies:
156
170
  - - "~>"
157
171
  - !ruby/object:Gem::Version
158
172
  version: '3.4'
159
- - !ruby/object:Gem::Dependency
160
- name: toys
161
- requirement: !ruby/object:Gem::Requirement
162
- requirements:
163
- - - "~>"
164
- - !ruby/object:Gem::Version
165
- version: '0.11'
166
- type: :development
167
- prerelease: false
168
- version_requirements: !ruby/object:Gem::Requirement
169
- requirements:
170
- - - "~>"
171
- - !ruby/object:Gem::Version
172
- version: '0.11'
173
173
  - !ruby/object:Gem::Dependency
174
174
  name: yard
175
175
  requirement: !ruby/object:Gem::Requirement
@@ -201,7 +201,6 @@ files:
201
201
  - CONTRIBUTING.md
202
202
  - LICENSE
203
203
  - README.md
204
- - data/exec_standard_entrypoint.rb.erb
205
204
  - lib/appengine.rb
206
205
  - lib/appengine/env.rb
207
206
  - lib/appengine/exec.rb
@@ -221,14 +220,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
220
  requirements:
222
221
  - - ">="
223
222
  - !ruby/object:Gem::Version
224
- version: 2.4.0
223
+ version: 2.5.0
225
224
  required_rubygems_version: !ruby/object:Gem::Requirement
226
225
  requirements:
227
226
  - - ">="
228
227
  - !ruby/object:Gem::Version
229
228
  version: '0'
230
229
  requirements: []
231
- rubygems_version: 3.0.3
230
+ rubygems_version: 3.3.5
232
231
  signing_key:
233
232
  specification_version: 4
234
233
  summary: Google App Engine integration tools
@@ -1,116 +0,0 @@
1
- # Copyright 2019 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
- # http://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 "webrick"
16
- require "monitor"
17
- require "json"
18
-
19
- SECRET = <%= secret %>
20
- COMMAND = Array(<%= command %>)
21
-
22
- port = Integer ENV["PORT"]
23
- server = ::WEBrick::HTTPServer.new Port: port
24
-
25
- Status = ::Struct.new :out_lines, :err_lines, :exit_status, :pid, :start_time
26
- $status = Status.new [], [], nil, nil, nil
27
- $status.extend ::MonitorMixin
28
-
29
- def do_start
30
- $status.synchronize do
31
- return unless $status.pid.nil?
32
- end
33
- $stdout.puts "Executing: #{COMMAND.inspect}"
34
- $stdout.flush
35
- rout, wout = ::IO.pipe
36
- rerr, werr = ::IO.pipe
37
- ::Thread.new do
38
- rout.each_line do |line|
39
- $status.synchronize { $status.out_lines << line }
40
- $stdout.puts line
41
- $stdout.flush
42
- end
43
- end
44
- ::Thread.new do
45
- rerr.each_line do |line|
46
- $status.synchronize { $status.err_lines << line }
47
- $stderr.puts line
48
- $stderr.flush
49
- end
50
- end
51
- start_time = Time.now.to_i
52
- pid = ::Process.spawn *COMMAND, err: werr, out: wout
53
- werr.close
54
- wout.close
55
- $status.synchronize do
56
- $status.pid = pid
57
- $status.start_time = start_time
58
- end
59
- ::Thread.new do
60
- _pid, status = ::Process.wait2 pid
61
- $status.synchronize do
62
- $status.exit_status = status.exitstatus
63
- end
64
- end
65
- end
66
-
67
- def get_status outpos: 0, errpos: 0
68
- outlines, errlines, status, start_time =
69
- $status.synchronize do
70
- [
71
- $status.out_lines[outpos..-1],
72
- $status.err_lines[errpos..-1],
73
- $status.exit_status,
74
- $status.start_time
75
- ]
76
- end
77
- {
78
- "outpos" => outpos + outlines.size,
79
- "errpos" => errpos + errlines.size,
80
- "outlines" => outlines,
81
- "errlines" => errlines,
82
- "status" => status,
83
- "time" => Time.now.to_i - start_time
84
- }
85
- end
86
-
87
- server.mount_proc "/_ah/start" do |req, res|
88
- do_start
89
- end
90
-
91
- server.mount_proc "/#{SECRET}" do |req, res|
92
- do_start
93
- status = get_status outpos: req.query["outpos"].to_i,
94
- errpos: req.query["errpos"].to_i
95
- res.body = JSON.dump status
96
- end
97
-
98
- server.mount_proc "/#{SECRET}/kill" do |req, res|
99
- unless req.request_method == "POST"
100
- res.status = 404
101
- return
102
- end
103
- $status.synchronize do
104
- if $status.pid.nil?
105
- res.status = 404
106
- return
107
- end
108
- ::Process.kill "SIGTERM", $status.pid
109
- end
110
- end
111
-
112
- begin
113
- server.start
114
- ensure
115
- server.shutdown
116
- end