appengine 0.6.0 → 0.7.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: 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