appengine 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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