appengine 0.4.6 → 0.5.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: 1dbbd25a6e4b55d60b85a5189f80fc21f85c8c32373b6844db7dac59120bdf51
4
- data.tar.gz: 67f78c07751d309cc4ee5c34d7bf7eccf742fbfb8c4278dcca82b1c575eb6b2d
3
+ metadata.gz: f375994484124c538890ff34a4e841784b17084f61a7db9f1e47a189cdfbc9d6
4
+ data.tar.gz: 0e36f8b5ba553b7b7c2d82c1beb2d96dbe5a99374aa5709d2d27d9cf1f809a63
5
5
  SHA512:
6
- metadata.gz: dc568d95c6d361746c44b1b3f5483637dfa8a7a0431c0fcc7fd110add25a7ebde3a70c9c026061355085b24e7d16f7c1458138d90502769fff031061a0e1a701
7
- data.tar.gz: f16b46bcb0972db0bb0fa44770279708e5ca460e1d6585f60a76d11f55687e138996e644c3df1f8d90db4d54de7d54d903481b8a01ea345d00cce6f21f5d7be1
6
+ metadata.gz: cc71ebe02ab9c6efd9d1d2c57edc03a2c901e167e8828146695d637e54fe8bdec2ee9118360658e79e7eaa81d8ffca6c8450903c0670cb6633671055d653d295
7
+ data.tar.gz: ab181b1f667cb53f451ee0b2dd54174a3369a2f9ef5d94085742d4cbd3f755441606e24d47552f3d90604d01ba33fb45c246067e3299b0b700b2395fe2e7d993
data/.yardopts CHANGED
@@ -1,7 +1,8 @@
1
1
  --no-private
2
2
  --title=AppEngine
3
3
  --markup markdown
4
-
4
+ --markup-provider redcarpet
5
+ --main=README.md
5
6
  ./lib/**/*.rb
6
7
  -
7
8
  README.md
@@ -2,6 +2,14 @@
2
2
 
3
3
  This is the change history for the appengine gem.
4
4
 
5
+ ## v0.5.0 (2019-07-15)
6
+
7
+ * appengine:exec supports the App Engine standard environment.
8
+ * appengine:exec supports setting the project via `GAE_PROJECT`.
9
+ * Support for an alternate appengine:exec strategy for flexible environment apps that talk to a database via a private IP.
10
+ * Fix crash when the gcloud path includes directories with spaces.
11
+ * Escape `$` symbols in environment configs. (tpbowden)
12
+
5
13
  ## v0.4.6 (2018-09-17)
6
14
 
7
15
  * Use gcloud builds submit instead of gcloud container builds submit. (tbpg)
data/README.md CHANGED
@@ -1,6 +1,9 @@
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
+ [![Gem Version](https://badge.fury.io/rb/appengine.svg)](https://badge.fury.io/rb/appengine)
6
+
4
7
  This repository contains the "appengine" gem, a collection of libraries and
5
8
  plugins for integrating Ruby apps with Google App Engine. It is not required
6
9
  for deploying a ruby application to Google App Engine, but it provides a
@@ -18,7 +21,7 @@ Currently, it includes:
18
21
  * Convenient access to environment information such as project ID and VM
19
22
  properties.
20
23
 
21
- Planned for the near future:
24
+ Potential future directions:
22
25
 
23
26
  * Tools for generating "app.yaml" configuration files for Ruby applications.
24
27
  * Streamlined implementation of health checks and other lifecycle hooks.
@@ -64,17 +67,25 @@ Rakefile:
64
67
 
65
68
  require "appengine/tasks"
66
69
 
67
- ### Necessary permissions for remote execution rake tasks
68
-
69
- If you are using the `appengine:exec` rake task, you may need to grant
70
- additional permissions to the Cloud Container Builder service account that
71
- runs the task, especially if you are using Cloud SQL (which is not covered by
72
- the default permissions granted to the account). If your task is failing with
73
- authorization errors, open the
74
- [IAM tab](https://console.cloud.google.com/iam-admin/iam/project) of the
75
- cloud console, select your project, and find the service account with the name
76
- `[your-project-number]@cloudbuild.gserviceaccount.com`. Add the Project Editor
77
- role to this service account.
70
+ ### Setting up appengine:exec remote execution
71
+
72
+ This gem is commonly used for its `appengine:exec` Rake task that provides a
73
+ way to run production tasks such as database migrations in the cloud. If you
74
+ are getting started with this feature, you should read the documentation
75
+ (available on the
76
+ [AppEngine::Exec module](http://www.rubydoc.info/gems/appengine/AppEngine/Exec))
77
+ carefully, for important tips. In particular:
78
+
79
+ * The strategy used by the gem is different depending on whether your app is
80
+ deployed to the App Engine standard environment or flexible environment.
81
+ It is important to understand which strategy is in use, because it affects
82
+ which version of your application code is used to run the task, and various
83
+ other factors.
84
+ * You may need to grant additional permissions to the service account that
85
+ runs the task. Again, the documentation will describe this in detail.
86
+ * If your app is running on the flexible environment and uses a VPC (and
87
+ connects to your database via a private IP address), then you will need to
88
+ use a special configuration for the task.
78
89
 
79
90
  ## Using this library
80
91
 
@@ -92,7 +103,9 @@ monitoring features of Google App Engine, see:
92
103
 
93
104
  Rails applications automatically activate this instrumentation when the gem
94
105
  is present. You may opt out of individual services by providing appropriate
95
- Rails configuration. See {AppEngine::Railtie} for more information.
106
+ Rails configuration. See
107
+ [AppEngine::Railtie](http://www.rubydoc.info/gems/appengine/AppEngine/Railtie)
108
+ for more information.
96
109
 
97
110
  Non-Rails applications must provide initialization code to activate this
98
111
  instrumentation, typically by installing a Rack middleware. You can find the
@@ -110,24 +123,19 @@ example, you could run a production database migration in a Rails app using:
110
123
 
111
124
  bundle exec rake appengine:exec -- bundle exec rake db:migrate
112
125
 
113
- The migration would be run in VMs provided by Google Cloud. It uses a
114
- privileged service account that will have access to the production cloud
115
- resources, such as Cloud SQL instances, used by the application. This mechanism
116
- is often much easier and safer than running the task on a local workstation and
117
- granting that workstation direct access to those Cloud SQL instances.
126
+ The migration would be run in containers on Google Cloud infrastructure, which
127
+ is much easier and safer than running the task on a local workstation and
128
+ granting that workstation direct access to your production database.
118
129
 
119
- See {AppEngine::Exec} for more information on App Engine remote execution.
130
+ See [AppEngine::Exec](http://www.rubydoc.info/gems/appengine/AppEngine/Exec)
131
+ for more information on App Engine remote execution.
120
132
 
121
- See {AppEngine::Tasks} for more information on running the rake tasks. The
122
- tasks are available automatically in Rails applications when the gem is
123
- present. Non-Rails applications may install the tasks by adding the line
133
+ See [AppEngine::Tasks](http://www.rubydoc.info/gems/appengine/AppEngine/Tasks)
134
+ for more information on running the Rake tasks. The tasks are available
135
+ automatically in Rails applications when the gem is present. Non-Rails
136
+ applications may install the tasks by adding the line
124
137
  `require "appengine/tasks"` to the `Rakefile`.
125
138
 
126
- Note that you may need to grant additional roles to the Container Builder
127
- service account that runs your tasks. If your task is failing with API
128
- authorization errors, try granting the Project Editor role to the CloudBuild
129
- service account.
130
-
131
139
  ## Development and support
132
140
 
133
141
  The source code for this gem is available on Github at
@@ -0,0 +1,116 @@
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
@@ -1,4 +1,6 @@
1
- # Copyright 2016 Google Inc. All rights reserved.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Google LLC
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -11,17 +13,22 @@
11
13
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
- ;
15
16
 
16
- # # Google AppEngine integration
17
+
18
+ ##
19
+ # ## Google AppEngine integration
17
20
  #
18
21
  # The AppEngine module includes optional tools helping Ruby applications to
19
22
  # integrate more closely with the Google App Engine environment.
20
-
23
+ #
21
24
  module AppEngine
25
+ ##
26
+ # Internal utilities
27
+ #
28
+ module Util
29
+ end
22
30
  end
23
31
 
24
-
25
32
  require "appengine/version"
26
33
  require "appengine/env"
27
34
  require "appengine/exec"
@@ -1,4 +1,6 @@
1
- # Copyright 2016 Google Inc. All rights reserved.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Google LLC
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -11,13 +13,11 @@
11
13
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
- ;
15
16
 
16
- require "google/cloud/env"
17
17
 
18
+ require "google/cloud/env"
18
19
 
19
20
  module AppEngine
20
-
21
21
  ##
22
22
  # A convenience object that provides information on the Google Cloud
23
23
  # hosting environment. For example, you can call
@@ -34,5 +34,4 @@ module AppEngine
34
34
  # directly instead. See the documentation for the `google-cloud-env` gem.
35
35
  #
36
36
  Env = ::Google::Cloud.env
37
-
38
37
  end
@@ -1,4 +1,6 @@
1
- # Copyright 2017 Google Inc. All rights reserved.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Google LLC
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -11,18 +13,19 @@
11
13
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
- ;
15
16
 
16
- require "yaml"
17
+
18
+ require "erb"
17
19
  require "json"
20
+ require "net/http"
21
+ require "securerandom"
18
22
  require "shellwords"
19
23
  require "tempfile"
24
+ require "yaml"
20
25
 
21
26
  require "appengine/util/gcloud"
22
27
 
23
-
24
28
  module AppEngine
25
-
26
29
  ##
27
30
  # # App Engine remote execution
28
31
  #
@@ -33,90 +36,214 @@ module AppEngine
33
36
  #
34
37
  # ## About App Engine execution
35
38
  #
36
- # App Engine execution spins up an image of a deployed App Engine app, and
37
- # runs a command in that image. For example, if your app runs on Ruby on
38
- # Rails, then your app provides a `bin/rails` tool, and you may invoke it
39
- # using App Engine execution---for example to run a command such as
40
- # `bundle exec bin/rails db:migrate` in the image.
39
+ # App Engine execution spins up a one-off copy of an App Engine app, and runs
40
+ # a command against it. For example, if your app runs on Ruby on Rails, then
41
+ # you might use App Engine execution to run a command such as
42
+ # `bundle exec bin/rails db:migrate` in production infrastructure (to avoid
43
+ # having to connect directly to your production database from a local
44
+ # workstation).
45
+ #
46
+ # App Engine execution provides two strategies for generating that "one-off
47
+ # copy":
41
48
  #
42
- # When App Engine execution runs your command, it provides access to key
43
- # elements of the App Engine environment, including:
49
+ # * A `deployment` strategy, which deploys a temporary version of your app
50
+ # to a single backend instance and runs the command there.
51
+ # * A `cloud_build` strategy, which deploys your application image to
52
+ # Google Cloud Build and runs the command there.
44
53
  #
45
- # * The same runtime that runs your application in App Engine itself.
46
- # * Any Cloud SQL connections requested by your application.
47
- # * Any environment variables set by your application.
54
+ # Both strategies are generally designed to emulate the App Engine runtime
55
+ # environment on cloud VMs similar to those used by actual deployments of
56
+ # your app. Both provide your application code and environment variables, and
57
+ # both provide access to Cloud SQL connections used by your app. However,
58
+ # they differ in what *version* of your app code they run against, and in
59
+ # certain other constraints and performance characteristics. More detailed
60
+ # information on using the two strategies is provided in the sections below.
48
61
  #
49
- # The command runs on virtual machines provided by Google Cloud Container
50
- # Builder, and has access to the credentials of the Cloud Container Builder
51
- # service account.
62
+ # Apps deployed to the App Engine *flexible environment* will use the
63
+ # `cloud_build` strategy by default. However, you can force an app to use the
64
+ # `deployment` strategy instead. (You might do so if you need to connect to a
65
+ # Cloud SQL database on a VPC using a private IP, because the `cloud_build`
66
+ # strategy does not support private IPs.) To force use of `deployment`, set
67
+ # the `strategy` parameter in the {AppEngine::Exec} constructor (or the
68
+ # corresponding `GAE_EXEC_STRATEGY` parameter in the Rake task). Note that
69
+ # the `deployment` strategy is usually significantly slower than
70
+ # `cloud_build` for apps in the flexible environment.
71
+ #
72
+ # Apps deployed to the App Engine *standard environment* will *always* use
73
+ # the `deployment` strategy. You cannot force use of the `cloud_build`
74
+ # strategy.
52
75
  #
53
76
  # ## Prerequisites
54
77
  #
55
78
  # To use App Engine remote execution, you will need:
56
79
  #
57
80
  # * An app deployed to Google App Engine, of course!
58
- # * The gcloud SDK installed and configured. See https://cloud.google.com/sdk/
81
+ # * The [gcloud SDK](https://cloud.google.com/sdk/) installed and configured.
59
82
  # * The `appengine` gem.
60
83
  #
61
- # You may also need to grant the Cloud Container Builder service account
62
- # any permissions needed by your command. Often, Project Editor permissions
63
- # will be sufficient for most tasks. You can find the service account
64
- # configuration in the IAM tab in the Cloud Console under the name
65
- # `[your-project-number]@cloudbuild.gserviceaccount.com`.
66
- #
67
84
  # You may use the `AppEngine::Exec` class to run commands directly. However,
68
85
  # in most cases, it will be easier to run commands via the provided rake
69
- # tasks. See {AppEngine::Tasks} for more info.
86
+ # tasks (see {AppEngine::Tasks}).
87
+ #
88
+ # ## Using the "deployment" strategy
89
+ #
90
+ # The `deployment` strategy deploys a temporary version of your app to a
91
+ # single backend App Engine instance, runs the command there, and then
92
+ # deletes the temporary version when it is finished.
93
+ #
94
+ # This is the default strategy (and indeed the only option) for apps running
95
+ # on the App Engine standard environment. It can also be used for flexible
96
+ # environment apps, but this is not commonly done because deployment of
97
+ # flexible environment apps can take a long time.
70
98
  #
71
- # ## Configuring
99
+ # Because the `deployment` strategy deploys a temporary version of your app,
100
+ # it runs against the *current application code* present where the command
101
+ # was initiated (i.e. the code currently on your workstation if you run the
102
+ # rake task from your workstation, or the current code on the branch if you
103
+ # are running from a CI/CD system.) This may be different from the code
104
+ # actually running in production, so it is important that you run from a
105
+ # compatible code branch.
72
106
  #
73
- # This class uses three parameters to specify which application image to use
74
- # to run your command: `service`, `config_path`, and `version`.
107
+ # ### Specifying the host application
75
108
  #
76
- # In most cases, you can use the defaults. The Exec class will look in your
77
- # current directory for a file called `./app.yaml` which describes your App
78
- # Engine service. It gets the service name from this file (or uses the
79
- # "default" name if none is specified), then looks up the most recently
80
- # created deployment version for that service. That deployment version then
81
- # provides the application image that runs your command.
109
+ # The `deployment` strategy works by deploying a temporary version of your
110
+ # app, so that it has access to your app's project and settings in App
111
+ # Engine. In most cases, it can determine this automatically, but depending
112
+ # on how your app or environment is structured, you may need to give it some
113
+ # help.
82
114
  #
83
- # If your app has multiple services, you may specify which config file
84
- # (other than `./app.yaml`) describes the desired service, by providing the
85
- # `config_path` parameter. Alternately, you may specify a service name
86
- # directly by providing the `service` parameter. If you provide both
87
- # parameters, `service` takes precedence.
115
+ # By default, your Google Cloud project is taken from the current gcloud
116
+ # project. If you need to override this, set the `:project` parameter in the
117
+ # {AppEngine::Exec} constructor (or the corresponding `GAE_PROJECT`
118
+ # parameter in the Rake task).
88
119
  #
89
- # Usually, App Engine execution uses the image for the most recently created
90
- # version of the service. (Note: the most recently created version is used,
91
- # regardless of whether that version is currently receiving traffic.) If you
92
- # want to use the image for a different version, you may specify a version
93
- # by providing the `version` parameter.
120
+ # By default, the service name is taken from the App Engine config file.
121
+ # App Engine execution will assume this file is called `app.yaml` in the
122
+ # current directory. To use a different config file, set the `config_path`
123
+ # parameter in the {AppEngine::Exec} constructor (or the corresponding
124
+ # `GAE_CONFIG` parameter in the Rake task). You may also set the service name
125
+ # directly, using the `service` parameter (or `GAE_SERVICE` in Rake).
126
+ #
127
+ # ### Providing credentials
128
+ #
129
+ # Your command will effectively be a deployment of your App Engine app
130
+ # itself, and will have access to the same credentials. For example, App
131
+ # Engine provides a service account by default for your app, or your app may
132
+ # be making use of its own service account key. In either case, make sure the
133
+ # service account has sufficient access for the command you want to run
134
+ # (such as database admin credentials).
135
+ #
136
+ # ### Other options
94
137
  #
95
138
  # You may also provide a timeout, which is the length of time that App
96
139
  # Engine execution will allow your command to run before it is considered to
97
140
  # have stalled and is terminated. The timeout should be a string of the form
98
141
  # `2h15m10s`. The default is `10m`.
99
142
  #
100
- # ## Resource usage and billing
143
+ # The timeout is set via the `timeout` parameter to the {AppEngine::Exec}
144
+ # constructor, or by setting the `GAE_TIMEOUT` environment variable when
145
+ # invoking using Rake.
146
+ #
147
+ # ### Resource usage and billing
148
+ #
149
+ # The `deployment` strategy deploys to a temporary instance of your app in
150
+ # order to run the command. You may be billed for that usage. However, the
151
+ # cost should be minimal, because it will then immediately delete that
152
+ # instance in order to minimize usage.
153
+ #
154
+ # If you interrupt the execution (or it crashes), it is possible that the
155
+ # temporary instance may not get deleted properly. If you suspect this may
156
+ # have happened, go to the App Engine tab in the cloud console, under
157
+ # "versions" of your service, and delete the temporary version manually. It
158
+ # will have a name matching the pattern `appengine-exec-<timestamp>`.
159
+ #
160
+ # ## Using the "cloud_build" strategy
161
+ #
162
+ # The `cloud_build` strategy takes the application image that App Engine is
163
+ # actually using to run your app, and uses it to spin up a copy of your app
164
+ # in [Google Cloud Build](https://cloud.google.com/cloud-build) (along with
165
+ # an emulation layer that emulates certain App Engine services such as Cloud
166
+ # SQL connection sockets). The command then gets run in the Cloud Build
167
+ # environment.
168
+ #
169
+ # This is the default strategy for apps running on the App Engine flexible
170
+ # environment. (It is not available for standard environment apps.) Note that
171
+ # the `cloud_build` strategy cannot be used if your command needs to connect
172
+ # to a database over a [VPC](https://cloud.google.com/vpc/) private IP
173
+ # address. This is because it runs on virtual machines provided by the Cloud
174
+ # Build service, which are not part of your VPC. If your database can be
175
+ # accessed only over a private IP, you should use the `deployment` strategy
176
+ # instead.
177
+ #
178
+ # ### Specifying the host application
179
+ #
180
+ # The `cloud_build` strategy needs to know exactly which app, service, and
181
+ # version of your app, to identify the application image to use.
182
+ #
183
+ # By default, your Google Cloud project is taken from the current gcloud
184
+ # project. If you need to override this, set the `:project` parameter in the
185
+ # {AppEngine::Exec} constructor (or the corresponding `GAE_PROJECT`
186
+ # parameter in the Rake task).
187
+ #
188
+ # By default, the service name is taken from the App Engine config file.
189
+ # App Engine execution will assume this file is called `app.yaml` in the
190
+ # current directory. To use a different config file, set the `config_path`
191
+ # parameter in the {AppEngine::Exec} constructor (or the corresponding
192
+ # `GAE_CONFIG` parameter in the Rake task). You may also set the service name
193
+ # directly, using the `service` parameter (or `GAE_SERVICE` in Rake).
194
+ #
195
+ # By default, the image of the most recently deployed version of your app is
196
+ # used. (Note that this most recently deployed version may not be the same
197
+ # version that is currently receiving traffic: for example, if you deployed
198
+ # with `--no-promote`.) To use a different version, set the `version`
199
+ # parameter in the {AppEngine::Exec} constructor (or the corresponding
200
+ # `GAE_VERSION` parameter in the Rake task).
101
201
  #
102
- # App Engine remote execution uses virtual machine resources provided by
103
- # Google Cloud Container Builder. Generally, a certain number of usage
104
- # minutes per day is covered under a free tier, but additional compute usage
105
- # beyond that time is billed to your Google Cloud account. For more details,
106
- # see https://cloud.google.com/container-builder/pricing
202
+ # ### Providing credentials
203
+ #
204
+ # By default, the `cloud_build` strategy uses your project's Cloud Build
205
+ # service account for its credentials. Unless your command provides its own
206
+ # service account key, you may need to grant the Cloud Build service account
207
+ # any permissions needed to execute your command (such as access to your
208
+ # database). For most tasks, it is sufficient to grant Project Editor
209
+ # permissions to the service account. You can find the service account
210
+ # configuration in the IAM tab in the Cloud Console under the name
211
+ # `[your-project-number]@cloudbuild.gserviceaccount.com`.
212
+ #
213
+ # ### Other options
214
+ #
215
+ # You may also provide a timeout, which is the length of time that App
216
+ # Engine execution will allow your command to run before it is considered to
217
+ # have stalled and is terminated. The timeout should be a string of the form
218
+ # `2h15m10s`. The default is `10m`.
219
+ #
220
+ # The timeout is set via the `timeout` parameter to the {AppEngine::Exec}
221
+ # constructor, or by setting the `GAE_TIMEOUT` environment variable when
222
+ # invoking using Rake.
223
+ #
224
+ # You can also set the wrapper image used to emulate the App Engine runtime
225
+ # environment, by setting the `wrapper_image` parameter to the constructor,
226
+ # or by setting the `GAE_EXEC_WRAPPER_IMAGE` environment variable. Generally,
227
+ # you will not need to do this unless you are testing a new wrapper image.
228
+ #
229
+ # ### Resource usage and billing
230
+ #
231
+ # The `cloud_build` strategy uses virtual machine resources provided by
232
+ # Google Cloud Build. Generally, a certain number of usage minutes per day is
233
+ # covered under a free tier, but additional compute usage beyond that time is
234
+ # billed to your Google Cloud account. For more details,
235
+ # see https://cloud.google.com/cloud-build/pricing
107
236
  #
108
237
  # If your command makes API calls or utilizes other cloud resources, you may
109
- # also be billed for that usage. However, remote execution does not use
110
- # actual App Engine instances, and you will not be billed for additional App
111
- # Engine instance usage.
238
+ # also be billed for that usage. However, the `cloud_build` strategy (unlike
239
+ # the `deployment` strategy) does not use actual App Engine instances, and
240
+ # you will not be billed for additional App Engine instance usage.
112
241
  #
113
242
  class Exec
114
-
115
- @default_timeout = "10m".freeze
116
- @default_service = "default".freeze
117
- @default_config_path = "./app.yaml".freeze
118
- @default_wrapper_image = "gcr.io/google-appengine/exec-wrapper:latest".freeze
119
-
243
+ @default_timeout = "10m"
244
+ @default_service = "default"
245
+ @default_config_path = "./app.yaml"
246
+ @default_wrapper_image = "gcr.io/google-appengine/exec-wrapper:latest"
120
247
 
121
248
  ##
122
249
  # Base class for exec-related usage errors.
@@ -124,6 +251,41 @@ module AppEngine
124
251
  class UsageError < ::StandardError
125
252
  end
126
253
 
254
+ ##
255
+ # Unsupported strategy
256
+ #
257
+ class UnsupportedStrategy < UsageError
258
+ def initialize strategy, app_env
259
+ @strategy = strategy
260
+ @app_env = app_env
261
+ super "Strategy \"#{strategy}\" not supported for the #{app_env}" \
262
+ " environment"
263
+ end
264
+ attr_reader :strategy
265
+ attr_reader :app_env
266
+ end
267
+
268
+ ##
269
+ # Exception raised when a parameter is malformed.
270
+ #
271
+ class BadParameter < UsageError
272
+ def initialize param, value
273
+ @param_name = param
274
+ @value = value
275
+ super "Bad value for #{param}: #{value}"
276
+ end
277
+ attr_reader :param_name
278
+ attr_reader :value
279
+ end
280
+
281
+ ##
282
+ # Exception raised when gcloud has no default project.
283
+ #
284
+ class NoDefaultProject < UsageError
285
+ def initialize
286
+ super "No default project set."
287
+ end
288
+ end
127
289
 
128
290
  ##
129
291
  # Exception raised when the App Engine config file could not be found.
@@ -152,7 +314,7 @@ module AppEngine
152
314
  # versions at all could be found for the given service.
153
315
  #
154
316
  class NoSuchVersion < UsageError
155
- def initialize service, version=nil
317
+ def initialize service, version = nil
156
318
  @service = service
157
319
  @version = version
158
320
  if version
@@ -165,25 +327,7 @@ module AppEngine
165
327
  attr_reader :version
166
328
  end
167
329
 
168
- ##
169
- # Exception raised when an explicitly-specified service name conflicts with
170
- # a config-specified service name.
171
- #
172
- class ServiceNameConflict < UsageError
173
- def initialize service_name, config_name, config_path
174
- @service_name = service_name
175
- @config_name = config_name
176
- @config_path = config_path
177
- super "Service name conflicts with config file"
178
- end
179
- attr_reader :service_name
180
- attr_reader :config_name
181
- attr_reader :config_path
182
- end
183
-
184
-
185
330
  class << self
186
-
187
331
  ## @return [String] Default command timeout.
188
332
  attr_accessor :default_timeout
189
333
 
@@ -207,79 +351,133 @@ module AppEngine
207
351
  # the service name from the config file.
208
352
  # @param config_path [String,nil] App Engine config file to get the
209
353
  # service name from if the service name is not provided directly.
210
- # Defaults to the value of `AppEngine::Exec.default_config_path`.
211
- # @param version [String,nil] Version string. Defaults to the most
212
- # recently created version of the given service (which may not be the
213
- # one currently receiving traffic).
214
- # @param timeout [String,nil] Timeout string. Defaults to the value of
215
- # `AppEngine::Exec.default_timeout`.
354
+ # If omitted, defaults to the value returned by
355
+ # {AppEngine::Exec.default_config_path}.
356
+ # @param version [String,nil] Version string. If omitted, defaults to the
357
+ # most recently created version of the given service (which may not
358
+ # be the one currently receiving traffic).
359
+ # @param timeout [String,nil] Timeout string. If omitted, defaults to the
360
+ # value returned by {AppEngine::Exec.default_timeout}.
361
+ # @param wrapper_image [String,nil] The fully qualified name of the
362
+ # wrapper image to use. (Applies only to the "cloud_build" strategy.)
363
+ # @param strategy [String,nil] The execution strategy to use, or `nil` to
364
+ # choose a default based on the App Engine environment (flexible or
365
+ # standard). Allowed values are `nil`, `"deployment"` (which is the
366
+ # default for Standard), and `"cloud_build"` (which is the default
367
+ # for Flexible).
216
368
  #
217
369
  def new_rake_task name, args: [], env_args: [],
218
370
  service: nil, config_path: nil, version: nil,
219
- timeout: nil
220
- escaped_args = args.map{ |arg|
221
- arg.gsub(/[,\[\]]/){ |m| "\\#{m}" }
222
- }
223
- if escaped_args.empty?
224
- name_with_args = name
225
- else
226
- name_with_args = "#{name}[#{escaped_args.join ','}]"
371
+ timeout: nil, project: nil, wrapper_image: nil,
372
+ strategy: nil
373
+ escaped_args = args.map do |arg|
374
+ arg.gsub(/[,\[\]]/) { |m| "\\#{m}" }
227
375
  end
376
+ name_with_args =
377
+ if escaped_args.empty?
378
+ name
379
+ else
380
+ "#{name}[#{escaped_args.join ','}]"
381
+ end
228
382
  new ["bundle", "exec", "rake", name_with_args] + env_args,
229
383
  service: service, config_path: config_path, version: version,
230
- timeout: timeout
384
+ timeout: timeout, project: project, wrapper_image: wrapper_image,
385
+ strategy: strategy
231
386
  end
232
-
233
387
  end
234
388
 
235
-
236
389
  ##
237
390
  # Create an execution for the given command.
238
391
  #
239
392
  # @param command [Array<String>] The command in array form.
393
+ # @param project [String,nil] ID of the project. If omitted, obtains
394
+ # the project from gcloud.
240
395
  # @param service [String,nil] Name of the service. If omitted, obtains
241
396
  # the service name from the config file.
242
397
  # @param config_path [String,nil] App Engine config file to get the
243
398
  # service name from if the service name is not provided directly.
244
- # Defaults to the value of `AppEngine::Exec.default_config_path`.
245
- # @param version [String,nil] Version string. Defaults to the most
246
- # recently created version of the given service (which may not be the
247
- # one currently receiving traffic).
248
- # @param timeout [String,nil] Timeout string. Defaults to the value of
249
- # `AppEngine::Exec.default_timeout`.
399
+ # If omitted, defaults to the value returned by
400
+ # {AppEngine::Exec.default_config_path}.
401
+ # @param version [String,nil] Version string. If omitted, defaults to the
402
+ # most recently created version of the given service (which may not be
403
+ # the one currently receiving traffic).
404
+ # @param timeout [String,nil] Timeout string. If omitted, defaults to the
405
+ # value returned by {AppEngine::Exec.default_timeout}.
406
+ # @param wrapper_image [String,nil] The fully qualified name of the wrapper
407
+ # image to use. (Applies only to the "cloud_build" strategy.)
408
+ # @param strategy [String,nil] The execution strategy to use, or `nil` to
409
+ # choose a default based on the App Engine environment (flexible or
410
+ # standard). Allowed values are `nil`, `"deployment"` (which is the
411
+ # default for Standard), and `"cloud_build"` (which is the default for
412
+ # Flexible).
250
413
  #
251
414
  def initialize command,
252
- service: nil, config_path: nil, version: nil, timeout: nil,
253
- wrapper_image: nil
415
+ project: nil, service: nil, config_path: nil, version: nil,
416
+ timeout: nil, wrapper_image: nil, strategy: nil
254
417
  @command = command
255
418
  @service = service
256
419
  @config_path = config_path
257
420
  @version = version
258
421
  @timeout = timeout
422
+ @project = project
259
423
  @wrapper_image = wrapper_image
424
+ @strategy = strategy
260
425
 
261
426
  yield self if block_given?
262
427
  end
263
428
 
429
+ ##
430
+ # @return [String] The project ID.
431
+ # @return [nil] if the default gcloud project should be used.
432
+ #
433
+ attr_accessor :project
264
434
 
265
- ## @return [String,nil] The service name, or nil to read from the config.
435
+ ##
436
+ # @return [String] The service name.
437
+ # @return [nil] if the service should be obtained from the app config.
438
+ #
266
439
  attr_accessor :service
267
440
 
268
- ## @return [String,nil] Path to the config file, or nil to use the default.
441
+ ##
442
+ # @return [String] Path to the config file.
443
+ # @return [nil] if the default of `./app.yaml` should be used.
444
+ #
269
445
  attr_accessor :config_path
270
446
 
271
- ## @return [String,nil] Service version, or nil to use the most recent.
447
+ ##
448
+ # @return [String] Service version of the image to use.
449
+ # @return [nil] if the most recent should be used.
450
+ #
272
451
  attr_accessor :version
273
452
 
274
- ## @return [String,nil] Command timeout, or nil to use the default.
453
+ ##
454
+ # @return [String] The command timeout, in `1h23m45s` format.
455
+ # @return [nil] if the default of `10m` should be used.
456
+ #
275
457
  attr_accessor :timeout
276
458
 
277
- ## @return [String,Array<String>] Command to run.
459
+ ##
460
+ # The command to run.
461
+ #
462
+ # @return [String] if the command is a script to be run in a shell.
463
+ # @return [Array<String>] if the command is a posix command to be run
464
+ # directly without a shell.
465
+ #
278
466
  attr_accessor :command
279
467
 
280
- ## @return [String] Custom wrapper image to use, or nil to use the default.
468
+ ##
469
+ # @return [String] Custom wrapper image to use.
470
+ # @return [nil] if the default should be used.
471
+ #
281
472
  attr_accessor :wrapper_image
282
473
 
474
+ ##
475
+ # @return [String] The execution strategy to use. Allowed values are
476
+ # `"deployment"` and `"cloud_build"`.
477
+ # @return [nil] to choose a default based on the App Engine environment
478
+ # (flexible or standard).
479
+ #
480
+ attr_accessor :strategy
283
481
 
284
482
  ##
285
483
  # Executes the command synchronously. Streams the logs back to standard out
@@ -287,29 +485,15 @@ module AppEngine
287
485
  #
288
486
  def start
289
487
  resolve_parameters
290
-
291
- version_info = version_info @service, @version
292
- env_variables = version_info["envVariables"] || {}
293
- beta_settings = version_info["betaSettings"] || {}
294
- cloud_sql_instances = beta_settings["cloud_sql_instances"] || []
295
- image = version_info["deployment"]["container"]["image"]
296
-
297
- config = build_config command, image, env_variables, cloud_sql_instances
298
- file = ::Tempfile.new ["cloudbuild_", ".json"]
299
- begin
300
- ::JSON.dump config, file
301
- file.flush
302
- Util::Gcloud.execute [
303
- "builds", "submit",
304
- "--no-source",
305
- "--config=#{file.path}",
306
- "--timeout=#{@timeout}"]
307
- ensure
308
- file.close!
488
+ app_info = version_info @service, @version
489
+ resolve_strategy app_info["env"]
490
+ if @strategy == "cloud_build"
491
+ start_build_strategy app_info
492
+ else
493
+ start_deployment_strategy app_info
309
494
  end
310
495
  end
311
496
 
312
-
313
497
  private
314
498
 
315
499
  ##
@@ -317,61 +501,55 @@ module AppEngine
317
501
  # Resolves and canonicalizes all the parameters.
318
502
  #
319
503
  def resolve_parameters
320
- unless @command.is_a? Array
321
- @command = ::Shellwords.parse @command.to_s
322
- end
323
-
324
- config_service = config_path = nil
325
- if @config_path || !@service
326
- config_service = begin
327
- config_path = @config_path || Exec.default_config_path
328
- ::YAML.load_file(config_path)["service"] || Exec.default_service
329
- rescue ::Errno::ENOENT
330
- raise ConfigFileNotFound.new config_path
331
- rescue
332
- raise BadConfigFileFormat.new config_path
333
- end
334
- end
335
- if @service && config_service && @service != config_service
336
- raise ServiceNameConflict.new @service, config_service, config_path
337
- end
338
-
339
- @service ||= config_service
504
+ @timestamp_suffix = ::Time.now.strftime "%Y%m%d%H%M%S"
505
+ @command = ::Shellwords.parse @command.to_s unless @command.is_a? Array
506
+ @project ||= default_project
507
+ @service ||= service_from_config || Exec.default_service
340
508
  @version ||= latest_version @service
341
509
  @timeout ||= Exec.default_timeout
510
+ @timeout_seconds = parse_timeout @timeout
342
511
  @wrapper_image ||= Exec.default_wrapper_image
512
+ self
343
513
  end
344
514
 
345
- ##
346
- # @private
347
- # Builds a cloudbuild config as a data structure.
348
- #
349
- # @param command [Array<String>] The command in array form.
350
- # @param image [String] The fully qualified image path.
351
- # @param env_variables[Hash<String,String>] Environment variables.
352
- # @param cloud_sql_instances[String,Array<String>] Names of cloud sql
353
- # instances to connect to.
354
- #
355
- def build_config command, image, env_variables, cloud_sql_instances
356
- args = ["-i", image]
357
- env_variables.each do |k, v|
358
- args << "-e" << "#{k}=#{v}"
515
+ def resolve_strategy app_env
516
+ @strategy = @strategy.to_s.downcase
517
+ if @strategy.empty?
518
+ @strategy = app_env == "flexible" ? "cloud_build" : "deployment"
359
519
  end
360
- unless cloud_sql_instances.empty?
361
- cloud_sql_instances = Array(cloud_sql_instances)
362
- cloud_sql_instances.each do |sql|
363
- args << "-s" << sql
364
- end
520
+ if app_env == "standard" && @strategy == "cloud_build" ||
521
+ @strategy != "cloud_build" && @strategy != "deployment"
522
+ raise UnsupportedStrategy.new @strategy, app_env
365
523
  end
366
- args << "--"
367
- args += command
524
+ @strategy
525
+ end
368
526
 
369
- {
370
- "steps" => [
371
- "name" => @wrapper_image,
372
- "args" => args
373
- ]
374
- }
527
+ def service_from_config
528
+ return nil if !@config_path && @service
529
+ @config_path ||= Exec.default_config_path
530
+ ::YAML.load_file(config_path)["service"]
531
+ rescue ::Errno::ENOENT
532
+ raise ConfigFileNotFound, @config_path
533
+ rescue ::StandardError
534
+ raise BadConfigFileFormat, @config_path
535
+ end
536
+
537
+ def default_project
538
+ result = Util::Gcloud.execute \
539
+ ["config", "get-value", "project"],
540
+ capture: true, assert: false
541
+ result.strip!
542
+ raise NoDefaultProject if result.empty?
543
+ result
544
+ end
545
+
546
+ def parse_timeout timeout_str
547
+ matched = timeout_str =~ /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/
548
+ raise BadParameter.new "timeout", timeout_str unless matched
549
+ hours = ::Regexp.last_match(1).to_i
550
+ minutes = ::Regexp.last_match(2).to_i
551
+ seconds = ::Regexp.last_match(3).to_i
552
+ hours * 3600 + minutes * 60 + seconds
375
553
  end
376
554
 
377
555
  ##
@@ -383,15 +561,18 @@ module AppEngine
383
561
  # @return [String] Name of the most recent version.
384
562
  #
385
563
  def latest_version service
386
- result = Util::Gcloud.execute [
564
+ result = Util::Gcloud.execute \
565
+ [
387
566
  "app", "versions", "list",
388
- "--service=#{service}",
389
- "--format=get(version.id)",
390
- "--sort-by=~version.createTime",
391
- "--limit=1"],
392
- capture: true, assert: false
567
+ "--project", @project,
568
+ "--service", service,
569
+ "--format", "get(version.id)",
570
+ "--sort-by", "~version.createTime",
571
+ "--limit", "1"
572
+ ],
573
+ capture: true, assert: false
393
574
  result = result.split.first
394
- raise NoSuchVersion.new(service) unless result
575
+ raise NoSuchVersion, service unless result
395
576
  result
396
577
  end
397
578
 
@@ -403,23 +584,235 @@ module AppEngine
403
584
  # "default" is used.
404
585
  # @param version [String] Name of the version. If omitted, the most
405
586
  # recently deployed is used.
406
- # @return [Hash,nil] A collection of fields parsed from the JSON
407
- # representation of the version, or nil if the requested version
408
- # doesn't exist.
587
+ # @return [Hash] A collection of fields parsed from the JSON representation
588
+ # of the version
589
+ # @return [nil] if the requested version doesn't exist.
409
590
  #
410
591
  def version_info service, version
411
592
  service ||= "default"
412
593
  version ||= latest_version service
413
- result = Util::Gcloud.execute [
594
+ result = Util::Gcloud.execute \
595
+ [
414
596
  "app", "versions", "describe", version,
415
- "--service=#{service}",
416
- "--format=json"],
417
- capture: true, assert: false
597
+ "--project", @project,
598
+ "--service", service,
599
+ "--format", "json"
600
+ ],
601
+ capture: true, assert: false
418
602
  result.strip!
419
603
  raise NoSuchVersion.new(service, version) if result.empty?
420
604
  ::JSON.parse result
421
605
  end
422
606
 
423
- end
607
+ ##
608
+ # @private
609
+ # Performs exec on a GAE standard app.
610
+ #
611
+ def start_deployment_strategy app_info
612
+ describe_deployment_strategy
613
+ entrypoint_file = app_yaml_file = temp_version = nil
614
+ begin
615
+ puts "\n---------- DEPLOY COMMAND ----------"
616
+ secret = create_secret
617
+ entrypoint_file = copy_entrypoint secret
618
+ app_yaml_file = copy_app_yaml app_info, entrypoint_file
619
+ temp_version = deploy_temp_app app_yaml_file
620
+ puts "\n---------- EXECUTE COMMAND ----------"
621
+ puts "COMMAND: #{@command.inspect}\n\n"
622
+ exit_status = track_status temp_version, secret
623
+ puts "\nEXIT STATUS: #{exit_status}"
624
+ ensure
625
+ puts "\n---------- CLEANUP ----------"
626
+ ::File.unlink entrypoint_file if entrypoint_file
627
+ ::File.unlink app_yaml_file if app_yaml_file
628
+ delete_temp_version temp_version
629
+ end
630
+ end
631
+
632
+ def describe_deployment_strategy
633
+ puts "\nUsing the `deployment` strategy for appengine:exec"
634
+ puts "(i.e. deploying a temporary version of your app)"
635
+ puts "PROJECT: #{@project}"
636
+ puts "SERVICE: #{@service}"
637
+ puts "TIMEOUT: #{@timeout}"
638
+ end
639
+
640
+ def create_secret
641
+ ::SecureRandom.alphanumeric 20
642
+ end
643
+
644
+ def copy_entrypoint secret
645
+ entrypoint_template =
646
+ ::File.join(::File.dirname(::File.dirname(__dir__)),
647
+ "data", "exec_standard_entrypoint.rb.erb")
648
+ entrypoint_file = "appengine_exec_entrypoint_#{@timestamp_suffix}.rb"
649
+ erb = ::ERB.new ::File.read entrypoint_template
650
+ data = {
651
+ secret: secret.inspect, command: command.inspect
652
+ }
653
+ result = erb.result_with_hash data
654
+ ::File.open entrypoint_file, "w" do |file|
655
+ file.write result
656
+ end
657
+ entrypoint_file
658
+ end
659
+
660
+ def copy_app_yaml app_info, entrypoint_file
661
+ yaml_data = {
662
+ "runtime" => app_info["runtime"],
663
+ "service" => @service,
664
+ "entrypoint" => "ruby #{entrypoint_file}",
665
+ "env_variables" => app_info["envVariables"],
666
+ "manual_scaling" => { "instances" => 1 }
667
+ }
668
+ if app_info["env"] == "flexible"
669
+ complete_flex_app_yaml yaml_data, app_info
670
+ else
671
+ complete_standard_app_yaml yaml_data, app_info
672
+ end
673
+ app_yaml_file = "appengine_exec_config_#{@timestamp_suffix}.yaml"
674
+ ::File.open app_yaml_file, "w" do |file|
675
+ ::Psych.dump yaml_data, file
676
+ end
677
+ app_yaml_file
678
+ end
679
+
680
+ def complete_flex_app_yaml yaml_data, app_info
681
+ yaml_data["env"] = "flex"
682
+ orig_path = (app_info["betaSettings"] || {})["module_yaml_path"]
683
+ return unless orig_path
684
+ orig_yaml = ::YAML.load_file orig_path
685
+ copy_keys = ["skip_files", "resources", "network", "runtime_config",
686
+ "beta_settings"]
687
+ copy_keys.each do |key|
688
+ yaml_data[key] = orig_yaml[key] if orig_yaml[key]
689
+ end
690
+ end
691
+
692
+ def complete_standard_app_yaml yaml_data, app_info
693
+ yaml_data["instance_class"] = app_info["instanceClass"].sub(/^F/, "B")
694
+ end
695
+
696
+ def deploy_temp_app app_yaml_file
697
+ temp_version = "appengine-exec-#{@timestamp_suffix}"
698
+ Util::Gcloud.execute [
699
+ "app", "deploy", app_yaml_file,
700
+ "--project", @project,
701
+ "--version", temp_version,
702
+ "--no-promote", "--quiet"
703
+ ]
704
+ temp_version
705
+ end
706
+
707
+ def track_status temp_version, secret
708
+ host = "#{temp_version}.#{@service}.#{@project}.appspot.com"
709
+ ::Net::HTTP.start host do |http|
710
+ outpos = errpos = 0
711
+ delay = 0.0
712
+ loop do
713
+ sleep delay
714
+ uri = URI("http://#{host}/#{secret}")
715
+ uri.query = ::URI.encode_www_form outpos: outpos, errpos: errpos
716
+ response = http.request_get uri
717
+ data = ::JSON.parse response.body
718
+ data["outlines"].each { |line| puts "[STDOUT] #{line}" }
719
+ data["errlines"].each { |line| puts "[STDERR] #{line}" }
720
+ outpos = data["outpos"]
721
+ errpos = data["errpos"]
722
+ return data["status"] if data["status"]
723
+ if data["time"] > @timeout_seconds
724
+ http.request_post "/#{secret}/kill", ""
725
+ return "timeout"
726
+ end
727
+ if data["outlines"].empty? && data["errlines"].empty?
728
+ delay += 0.1
729
+ delay = 1.0 if delay > 1.0
730
+ else
731
+ delay = 0.0
732
+ end
733
+ end
734
+ end
735
+ end
736
+
737
+ def delete_temp_version temp_version
738
+ Util::Gcloud.execute [
739
+ "app", "versions", "delete", temp_version,
740
+ "--project", @project,
741
+ "--service", @service,
742
+ "--quiet"
743
+ ]
744
+ end
745
+
746
+ ##
747
+ # @private
748
+ # Performs exec on a GAE flexible app.
749
+ #
750
+ def start_build_strategy app_info
751
+ env_variables = app_info["envVariables"] || {}
752
+ beta_settings = app_info["betaSettings"] || {}
753
+ cloud_sql_instances = beta_settings["cloud_sql_instances"] || []
754
+ image = app_info["deployment"]["container"]["image"]
755
+
756
+ describe_build_strategy
757
+
758
+ config = build_config command, image, env_variables, cloud_sql_instances
759
+ file = ::Tempfile.new ["cloudbuild_", ".json"]
760
+ begin
761
+ ::JSON.dump config, file
762
+ file.flush
763
+ Util::Gcloud.execute [
764
+ "builds", "submit",
765
+ "--project", @project,
766
+ "--no-source",
767
+ "--config", file.path,
768
+ "--timeout", @timeout
769
+ ]
770
+ ensure
771
+ file.close!
772
+ end
773
+ end
774
+
775
+ def describe_build_strategy
776
+ puts "\nUsing the `cloud_build` strategy for appengine:exec"
777
+ puts "(i.e. running your app image in Cloud Build)"
778
+ puts "PROJECT: #{@project}"
779
+ puts "SERVICE: #{@service}"
780
+ puts "VERSION: #{@version}"
781
+ puts "TIMEOUT: #{@timeout}"
782
+ puts ""
783
+ end
424
784
 
785
+ ##
786
+ # @private
787
+ # Builds a cloudbuild config as a data structure.
788
+ #
789
+ # @param command [Array<String>] The command in array form.
790
+ # @param image [String] The fully qualified image path.
791
+ # @param env_variables [Hash<String,String>] Environment variables.
792
+ # @param cloud_sql_instances [String,Array<String>] Names of cloud sql
793
+ # instances to connect to.
794
+ #
795
+ def build_config command, image, env_variables, cloud_sql_instances
796
+ args = ["-i", image]
797
+ env_variables.each do |k, v|
798
+ v = v.gsub "$", "$$"
799
+ args << "-e" << "#{k}=#{v}"
800
+ end
801
+ unless cloud_sql_instances.empty?
802
+ cloud_sql_instances = Array(cloud_sql_instances)
803
+ cloud_sql_instances.each do |sql|
804
+ args << "-s" << sql
805
+ end
806
+ end
807
+ args << "--"
808
+ args += command
809
+
810
+ {
811
+ "steps" => [
812
+ "name" => @wrapper_image,
813
+ "args" => args
814
+ ]
815
+ }
816
+ end
817
+ end
425
818
  end