google-serverless-exec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9649c833b15ac827ec528c9834a0c44e1fd4f0f39c98f849fac9da49b469aee6
4
+ data.tar.gz: 7d848bb54ede49bc2758d028a777a68c6c3e73704b7a0325474ffa26a6e87861
5
+ SHA512:
6
+ metadata.gz: 621be644a9cc61b56697d6c8b28237e40c91cbda3105e73b3422714357698ee58afcbf7001ac669ae6d26785c462627acc8c6c59119d90ed52f814256d823102
7
+ data.tar.gz: '0568e46e630b6774612c7cc548e8d827eaeb975034ffefc53492c24eecb5baad3f7165d91c673fd8ef6d0644a70180e9ae268cf412517fa1aca91fe2fbe6c802'
data/.yardopts ADDED
@@ -0,0 +1,10 @@
1
+ --no-private
2
+ --title=Serverless Exec
3
+ --markup markdown
4
+ --markup-provider redcarpet
5
+ --main=README.md
6
+ ./lib/**/*.rb
7
+ -
8
+ README.md
9
+ CONTRIBUTING.md
10
+ CHANGELOG.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ### v0.1.0 / 2021-08-04
4
+
5
+ * Initial release
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,27 @@
1
+ Want to contribute? Great! First, read this page (including the small print at the end).
2
+
3
+ ### Before you contribute
4
+ Before we can use your code, you must sign the
5
+ [Google Individual Contributor License Agreement]
6
+ (https://cla.developers.google.com/about/google-individual)
7
+ (CLA), which you can do online. The CLA is necessary mainly because you own the
8
+ copyright to your changes, even after your contribution becomes part of our
9
+ codebase, so we need your permission to use and distribute your code. We also
10
+ need to be sure of various other things—for instance that you'll tell us if you
11
+ know that your code infringes on other people's patents. You don't have to sign
12
+ the CLA until after you've submitted your code for review and a member has
13
+ approved it, but you must do it before we can put your code into our codebase.
14
+ Before you start working on a larger contribution, you should get in touch with
15
+ us first through the issue tracker with your idea so that we can help out and
16
+ possibly guide you. Coordinating up front makes it much easier to avoid
17
+ frustration later on.
18
+
19
+ ### Code reviews
20
+ All submissions, including submissions by project members, require review. We
21
+ use Github pull requests for this purpose.
22
+
23
+ ### The small print
24
+ Contributions made by corporations are covered by a different agreement than
25
+ the one above, the
26
+ [Software Grant and Corporate Contributor License Agreement]
27
+ (https://cla.developers.google.com/about/google-corporate).
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Google Serverless Execution Tool
2
+
3
+ This repository contains the "google-serverless-exec" gem, a library for serverless execution. This may be used for safe running of ops and maintenance tasks, such as database migrations in a production serverless environment. It is not required for deploying a Ruby application to Google serverless compute, but it provides a number of convenience tools for integrating into Google serverless environments.
4
+
5
+ ## Quickstart
6
+
7
+ To install, include the "google-serverless-exec" gem in your `Gemfile`:
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem "google-serverless-exec"
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ ```ruby
17
+ $ bundle install
18
+ ```
19
+
20
+ ### Setting up serverless:exec execution
21
+
22
+ This library provides rake tasks for serverless execution, allowing
23
+ serverless applications to perform on-demand tasks in the serverless
24
+ environment. This may be used for safe running of ops and maintenance tasks,
25
+ such as database migrations, that access production cloud resources.
26
+
27
+ You can add the Rake tasks to your application by adding the following to your Rakefile:
28
+
29
+ ```ruby
30
+ require "google/serverless/exec/tasks"
31
+ ```
32
+
33
+ You can run a production database migration in a Rails app using:
34
+
35
+ bundle exec rake serverless:exec -- bundle exec rake db:migrate
36
+
37
+ The migration would be run in containers on Google Cloud infrastructure, which
38
+ is much easier and safer than running the task on a local workstation and
39
+ granting that workstation direct access to your production database.
40
+
41
+ ## Development
42
+
43
+ The source code for this gem is available on Github at https://github.com/GoogleCloudPlatform/serverless-exec-ruby
44
+
45
+ The Ruby Serverless Exec is open source under the Apache 2.0 license.
46
+ Contributions are welcome. Please see the contributing guide at
47
+ https://github.com/GoogleCloudPlatform/serverless-exec-ruby/blob/main/CONTRIBUTING.md
48
+
49
+ Report issues at https://github.com/GoogleCloudPlatform/serverless-exec-ruby/issues
@@ -0,0 +1,914 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Google LLC
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "date"
18
+ require "erb"
19
+ require "json"
20
+ require "net/http"
21
+ require "securerandom"
22
+ require "shellwords"
23
+ require "tempfile"
24
+ require "yaml"
25
+
26
+ require "google/serverless/exec/gcloud"
27
+
28
+ module Google
29
+ module Serverless
30
+ ##
31
+ # # Serverless execution tool
32
+ #
33
+ # This class provides a client for serverless execution, allowing
34
+ # Serverless applications to perform on-demand tasks in the serverless
35
+ # environment. This may be used for safe running of ops and maintenance
36
+ # tasks, such as database migrations, that access production cloud resources.
37
+ #
38
+ # ## About serverless execution tool
39
+ #
40
+ # Serverless execution spins up a one-off copy of a serverless app, and runs
41
+ # a command against it. For example, if your app runs on Ruby on Rails, then
42
+ # you might use serverless execution to run a command such as
43
+ # `bundle exec bin/rails db:migrate` in production infrastructure (to avoid
44
+ # having to connect directly to your production database from a local
45
+ # workstation).
46
+ #
47
+ # Serverless execution provides two strategies for generating that "one-off
48
+ # copy":
49
+ #
50
+ # * A `deployment` strategy, which deploys a temporary version of your app
51
+ # to a single backend instance and runs the command there.
52
+ # * A `cloud_build` strategy, which deploys your application image to
53
+ # Google Cloud Build and runs the command there.
54
+ #
55
+ # Both strategies are generally designed to emulate the runtime
56
+ # environment on cloud VMs similar to those used by actual deployments of
57
+ # your app. Both provide your application code and environment variables, and
58
+ # both provide access to Cloud SQL connections used by your app. However,
59
+ # they differ in what *version* of your app code they run against, and in
60
+ # certain other constraints and performance characteristics. More detailed
61
+ # information on using the two strategies is provided in the sections below.
62
+ #
63
+ # Apps deployed to the App Engine *flexible environment* and Cloud Run will use the
64
+ # `cloud_build` strategy by default. However, you can force an app to use the
65
+ # `deployment` strategy instead. (You might do so if you need to connect to a
66
+ # Cloud SQL database on a VPC using a private IP, because the `cloud_build`
67
+ # strategy does not support private IPs.) To force use of `deployment`, set
68
+ # the `strategy` parameter in the {Google::Serverless::Exec} constructor (or the
69
+ # corresponding `GAE_EXEC_STRATEGY` parameter in the Rake task). Note that
70
+ # the `deployment` strategy is usually significantly slower than
71
+ # `cloud_build` for apps in the flexible environment.
72
+ #
73
+ # Apps deployed to the App Engine *standard environment* will *always* use
74
+ # the `deployment` strategy. You cannot force use of the `cloud_build`
75
+ # strategy.
76
+ #
77
+ # ## Prerequisites
78
+ #
79
+ # To use this tool, you will need:
80
+ #
81
+ # * An app deployed to Google serverless compute, of course!
82
+ # * The [gcloud SDK](https://cloud.google.com/sdk/) installed and configured.
83
+ # * The `serverless` gem.
84
+ #
85
+ # You may use the `Google::Serverless::Exec` class to run commands directly. However,
86
+ # in most cases, it will be easier to run commands via the provided rake
87
+ # tasks (see {Google::Serverless::Tasks}).
88
+ #
89
+ # ## Using the "deployment" strategy
90
+ #
91
+ # The `deployment` strategy deploys a temporary version of your app to a
92
+ # single backend App Engine instance, runs the command there, and then
93
+ # deletes the temporary version when it is finished.
94
+ #
95
+ # This is the default strategy (and indeed the only option) for apps running
96
+ # on the App Engine standard environment. It can also be used for flexible
97
+ # environment apps, but this is not commonly done because deployment of
98
+ # flexible environment apps can take a long time.
99
+ #
100
+ # Because the `deployment` strategy deploys a temporary version of your app,
101
+ # it runs against the *current application code* present where the command
102
+ # was initiated (i.e. the code currently on your workstation if you run the
103
+ # rake task from your workstation, or the current code on the branch if you
104
+ # are running from a CI/CD system.) This may be different from the code
105
+ # actually running in production, so it is important that you run from a
106
+ # compatible code branch.
107
+ #
108
+ # ### Specifying the host application
109
+ #
110
+ # The `deployment` strategy works by deploying a temporary version of your
111
+ # app, so that it has access to your app's project and settings in App
112
+ # Engine. In most cases, it can determine this automatically, but depending
113
+ # on how your app or environment is structured, you may need to give it some
114
+ # help.
115
+ #
116
+ # By default, your Google Cloud project is taken from the current gcloud
117
+ # project. If you need to override this, set the `:project` parameter in the
118
+ # {Google::Serverless::Exec} constructor (or the corresponding `GAE_PROJECT`
119
+ # parameter in the Rake task).
120
+ #
121
+ # By default, the service name is taken from the App Engine config file.
122
+ # Serverless execution will assume this file is called `app.yaml` in the
123
+ # current directory. To use a different config file, set the `config_path`
124
+ # parameter in the {Google::Serverless::Exec} constructor (or the corresponding
125
+ # `GAE_CONFIG` parameter in the Rake task). You may also set the service name
126
+ # directly, using the `service` parameter (or `GAE_SERVICE` in Rake).
127
+ #
128
+ # ### Providing credentials
129
+ #
130
+ # Your command will effectively be a deployment of your serverless app
131
+ # itself, and will have access to the same credentials. For example, App
132
+ # Engine provides a service account by default for your app, or your app may
133
+ # be making use of its own service account key. In either case, make sure the
134
+ # service account has sufficient access for the command you want to run
135
+ # (such as database admin credentials).
136
+ #
137
+ # ### Other options
138
+ #
139
+ # You may also provide a timeout, which is the length of time that serverless
140
+ # execution will allow your command to run before it is considered to
141
+ # have stalled and is terminated. The timeout should be a string of the form
142
+ # `2h15m10s`. The default is `10m`.
143
+ #
144
+ # The timeout is set via the `timeout` parameter to the {Google::Serverless::Exec}
145
+ # constructor, or by setting the `GAE_TIMEOUT` environment variable when
146
+ # invoking using Rake.
147
+ #
148
+ # ### Resource usage and billing
149
+ #
150
+ # The `deployment` strategy deploys to a temporary instance of your app in
151
+ # order to run the command. You may be billed for that usage. However, the
152
+ # cost should be minimal, because it will then immediately delete that
153
+ # instance in order to minimize usage.
154
+ #
155
+ # If you interrupt the execution (or it crashes), it is possible that the
156
+ # temporary instance may not get deleted properly. If you suspect this may
157
+ # have happened, go to the App Engine tab in the cloud console, under
158
+ # "versions" of your service, and delete the temporary version manually. It
159
+ # will have a name matching the pattern `serverless-exec-<timestamp>`.
160
+ #
161
+ # ## Using the "cloud_build" strategy
162
+ #
163
+ # The `cloud_build` strategy takes the application image that App Engine or Cloud Run is
164
+ # actually using to run your app, and uses it to spin up a copy of your app
165
+ # in [Google Cloud Build](https://cloud.google.com/cloud-build) (along with
166
+ # an emulation layer that emulates certain serverless services such as Cloud
167
+ # SQL connection sockets). The command then gets run in the Cloud Build
168
+ # environment.
169
+ #
170
+ # This is the default strategy for apps running on the App Engine flexible and Cloud
171
+ # Run environments. (It is not available for standard environment apps.) Note that
172
+ # the `cloud_build` strategy cannot be used if your command needs to connect
173
+ # to a database over a [VPC](https://cloud.google.com/vpc/) private IP
174
+ # address. This is because it runs on virtual machines provided by the Cloud
175
+ # Build service, which are not part of your VPC. If your database can be
176
+ # accessed only over a private IP, you should use the `deployment` strategy
177
+ # instead.
178
+ #
179
+ # The Cloud Build log is output to the directory specified by
180
+ # "CLOUD_BUILD_GCS_LOG_DIR". (ex. "gs://BUCKET-NAME/FOLDER-NAME")
181
+ # By default, log directory name is
182
+ # "gs://[PROJECT_NUMBER].cloudbuild-logs.googleusercontent.com/".
183
+ #
184
+ # ### Specifying the host application
185
+ #
186
+ # The `cloud_build` strategy needs to know exactly which app, service, and
187
+ # version of your app, to identify the application image to use.
188
+ #
189
+ # By default, your Google Cloud project is taken from the current gcloud
190
+ # project. If you need to override this, set the `:project` parameter in the
191
+ # {Google::Serverless::Exec} constructor (or the corresponding `GAE_PROJECT`
192
+ # parameter in the Rake task).
193
+ #
194
+ # By default, the service name is taken from the App Engine config file.
195
+ # Serverless execution will assume this file is called `app.yaml` in the
196
+ # current directory. To use a different config file, set the `config_path`
197
+ # parameter in the {Google::Serverless::Exec} constructor (or the corresponding
198
+ # `GAE_CONFIG` parameter in the Rake task). You may also set the service name
199
+ # directly, using the `service` parameter (or `GAE_SERVICE` in Rake).
200
+ #
201
+ # By default, the image of the most recently deployed version of your app is
202
+ # used. (Note that this most recently deployed version may not be the same
203
+ # version that is currently receiving traffic: for example, if you deployed
204
+ # with `--no-promote`.) To use a different version, set the `version`
205
+ # parameter in the {Google::Serverless::Exec} constructor
206
+ # (or the corresponding `GAE_VERSION` parameter in the Rake task).
207
+ #
208
+ # ### Providing credentials
209
+ #
210
+ # By default, the `cloud_build` strategy uses your project's Cloud Build
211
+ # service account for its credentials. Unless your command provides its own
212
+ # service account key, you may need to grant the Cloud Build service account
213
+ # any permissions needed to execute your command (such as access to your
214
+ # database). For most tasks, it is sufficient to grant Project Editor
215
+ # permissions to the service account. You can find the service account
216
+ # configuration in the IAM tab in the Cloud Console under the name
217
+ # `[your-project-number]@cloudbuild.gserviceaccount.com`.
218
+ #
219
+ # ### Other options
220
+ #
221
+ # You may also provide a timeout, which is the length of time that
222
+ # serverless execution will allow your command to run before it is considered to
223
+ # have stalled and is terminated. The timeout should be a string of the form
224
+ # `2h15m10s`. The default is `10m`.
225
+ #
226
+ # The timeout is set via the `timeout` parameter to the {Google::Serverless::Exec}
227
+ # constructor, or by setting the `GAE_TIMEOUT` environment variable when
228
+ # invoking using Rake.
229
+ #
230
+ # You can also set the wrapper image used to emulate the App Engine runtime
231
+ # environment, by setting the `wrapper_image` parameter to the constructor,
232
+ # or by setting the `GAE_EXEC_WRAPPER_IMAGE` environment variable. Generally,
233
+ # you will not need to do this unless you are testing a new wrapper image.
234
+ #
235
+ # ### Resource usage and billing
236
+ #
237
+ # The `cloud_build` strategy uses virtual machine resources provided by
238
+ # Google Cloud Build. Generally, a certain number of usage minutes per day is
239
+ # covered under a free tier, but additional compute usage beyond that time is
240
+ # billed to your Google Cloud account. For more details,
241
+ # see https://cloud.google.com/cloud-build/pricing
242
+ #
243
+ # If your command makes API calls or utilizes other cloud resources, you may
244
+ # also be billed for that usage. However, the `cloud_build` strategy (unlike
245
+ # the `deployment` strategy) does not use actual App Engine instances, and
246
+ # you will not be billed for additional App Engine instance usage.
247
+ #
248
+ class Exec
249
+ @default_timeout = "10m"
250
+ @default_service = "default"
251
+ @default_config_path = "./app.yaml"
252
+ @default_wrapper_image = "gcr.io/google-appengine/exec-wrapper:latest"
253
+
254
+ APP_ENGINE = :app_engine
255
+ CLOUD_RUN = :cloud_run
256
+
257
+ ##
258
+ # Base class for exec-related usage errors.
259
+ #
260
+ class UsageError < ::StandardError
261
+ end
262
+
263
+ ##
264
+ # Unsupported strategy
265
+ #
266
+ class UnsupportedStrategy < UsageError
267
+ def initialize strategy, app_env
268
+ @strategy = strategy
269
+ @app_env = app_env
270
+ super "Strategy \"#{strategy}\" not supported for the #{app_env}" \
271
+ " environment"
272
+ end
273
+ attr_reader :strategy
274
+ attr_reader :app_env
275
+ end
276
+
277
+ ##
278
+ # Exception raised when a parameter is malformed.
279
+ #
280
+ class BadParameter < UsageError
281
+ def initialize param, value
282
+ @param_name = param
283
+ @value = value
284
+ super "Bad value for #{param}: #{value}"
285
+ end
286
+ attr_reader :param_name
287
+ attr_reader :value
288
+ end
289
+
290
+ ##
291
+ # Exception raised when gcloud has no default project.
292
+ #
293
+ class NoDefaultProject < UsageError
294
+ def initialize
295
+ super "No default project set."
296
+ end
297
+ end
298
+
299
+ ##
300
+ # Exception raised when the App Engine config file could not be found.
301
+ #
302
+ class ConfigFileNotFound < UsageError
303
+ def initialize config_path
304
+ @config_path = config_path
305
+ super "Config file #{config_path} not found."
306
+ end
307
+ attr_reader :config_path
308
+ end
309
+
310
+ ##
311
+ # Exception raised when the App Engine config file could not be parsed.
312
+ #
313
+ class BadConfigFileFormat < UsageError
314
+ def initialize config_path
315
+ @config_path = config_path
316
+ super "Config file #{config_path} malformed."
317
+ end
318
+ attr_reader :config_path
319
+ end
320
+
321
+ ##
322
+ # Exception raised when the given version could not be found, or no
323
+ # versions at all could be found for the given service.
324
+ #
325
+ class NoSuchVersion < UsageError
326
+ def initialize service, version = nil
327
+ @service = service
328
+ @version = version
329
+ if version
330
+ super "No such version \"#{version}\" for service \"#{service}\""
331
+ else
332
+ super "No versions found for service \"#{service}\""
333
+ end
334
+ end
335
+ attr_reader :service
336
+ attr_reader :version
337
+ end
338
+
339
+ class << self
340
+ ## @return [String] Default command timeout.
341
+ attr_accessor :default_timeout
342
+
343
+ ## @return [String] Default service name if the config doesn't specify.
344
+ attr_accessor :default_service
345
+
346
+ ## @return [String] Path to default config file.
347
+ attr_accessor :default_config_path
348
+
349
+ ## @return [String] Docker image that implements the app engine wrapper.
350
+ attr_accessor :default_wrapper_image
351
+
352
+ ##
353
+ # Create an execution for a rake task.
354
+ #
355
+ # @param name [String] Name of the task
356
+ # @param args [Array<String>] Args to pass to the task
357
+ # @param env_args [Array<String>] Environment variable settings, each
358
+ # of the form `NAME=value`.
359
+ # @param service [String,nil] Name of the service. If omitted, obtains
360
+ # the service name from the config file.
361
+ # @param config_path [String,nil] App Engine config file to get the
362
+ # service name from if the service name is not provided directly.
363
+ # If omitted, defaults to the value returned by
364
+ # {Google::Serverless::Exec.default_config_path}.
365
+ # @param version [String,nil] Version string. If omitted, defaults to the
366
+ # most recently created version of the given service (which may not
367
+ # be the one currently receiving traffic).
368
+ # @param timeout [String,nil] Timeout string. If omitted, defaults to the
369
+ # value returned by {Google::Serverless::Exec.default_timeout}.
370
+ # @param wrapper_image [String,nil] The fully qualified name of the
371
+ # wrapper image to use. (Applies only to the "cloud_build" strategy.)
372
+ # @param strategy [String,nil] The execution strategy to use, or `nil` to
373
+ # choose a default based on the App Engine (flexible or standard) or Cloud Run
374
+ # environments. Allowed values are `nil`, `"deployment"` (which is the
375
+ # default for App Engine Standard), and `"cloud_build"` (which is the default
376
+ # for App Engine Flexible and Cloud Run).
377
+ # @param gcs_log_dir [String,nil] GCS bucket name of the cloud build log
378
+ # when strategy is "cloud_build". (ex. "gs://BUCKET-NAME/FOLDER-NAME")
379
+ # @param product [Symbol] The serverless product to use. If omitted, defaults to
380
+ # the value returned by {Google::Serverless::Exec#default_product}
381
+ def new_rake_task name, args: [], env_args: [],
382
+ service: nil, config_path: nil, version: nil,
383
+ timeout: nil, project: nil, wrapper_image: nil,
384
+ strategy: nil, gcs_log_dir: nil, product: nil
385
+ escaped_args = args.map do |arg|
386
+ arg.gsub(/[,\[\]]/) { |m| "\\#{m}" }
387
+ end
388
+ name_with_args =
389
+ if escaped_args.empty?
390
+ name
391
+ else
392
+ "#{name}[#{escaped_args.join ','}]"
393
+ end
394
+ new ["bundle", "exec", "rake", name_with_args] + env_args,
395
+ service: service, config_path: config_path, version: version,
396
+ timeout: timeout, project: project, wrapper_image: wrapper_image,
397
+ strategy: strategy, gcs_log_dir: gcs_log_dir, product: product
398
+ end
399
+ end
400
+
401
+ ##
402
+ # Create an execution for the given command.
403
+ #
404
+ # @param command [Array<String>] The command in array form.
405
+ # @param project [String,nil] ID of the project. If omitted, obtains
406
+ # the project from gcloud.
407
+ # @param service [String,nil] Name of the service. If omitted, obtains
408
+ # the service name from the config file.
409
+ # @param config_path [String,nil] App Engine config file to get the
410
+ # service name from if the service name is not provided directly.
411
+ # If omitted, defaults to the value returned by
412
+ # {Google::Serverless::Exec.default_config_path}.
413
+ # @param version [String,nil] Version string. If omitted, defaults to the
414
+ # most recently created version of the given service (which may not be
415
+ # the one currently receiving traffic).
416
+ # @param timeout [String,nil] Timeout string. If omitted, defaults to the
417
+ # value returned by {Google::Serverless::Exec.default_timeout}.
418
+ # @param wrapper_image [String,nil] The fully qualified name of the wrapper
419
+ # image to use. (Applies only to the "cloud_build" strategy.)
420
+ # @param strategy [String,nil] The execution strategy to use, or `nil` to
421
+ # choose a default based on the App Engine environment (flexible or standard) or
422
+ # Cloud Run environments. Allowed values are `nil`, `"deployment"` (which is the
423
+ # default for App Engine Standard), and `"cloud_build"` (which is the default for
424
+ # App Engine Flexible and Cloud Run).
425
+ # @param gcs_log_dir [String,nil] GCS bucket name of the cloud build log
426
+ # when strategy is "cloud_build". (ex. "gs://BUCKET-NAME/FOLDER-NAME")
427
+ # @param product [Symbol] The serverless product. If omitted, defaults to the
428
+ # value returns by {Google::Serverless::Exec#default_product}.
429
+ # Allowed values are {APP_ENGINE} and {CLOUD_RUN}.
430
+ #
431
+ def initialize command,
432
+ project: nil, service: nil, config_path: nil, version: nil,
433
+ timeout: nil, wrapper_image: nil, strategy: nil, gcs_log_dir: nil, product: nil
434
+ @command = command
435
+ @service = service
436
+ @config_path = config_path
437
+ @version = version
438
+ @timeout = timeout
439
+ @project = project
440
+ @wrapper_image = wrapper_image
441
+ @strategy = strategy
442
+ @gcs_log_dir = gcs_log_dir
443
+ @product = product
444
+
445
+ yield self if block_given?
446
+ end
447
+
448
+ ##
449
+ # @return [String] The project ID.
450
+ # @return [nil] if the default gcloud project should be used.
451
+ #
452
+ attr_accessor :project
453
+
454
+ ##
455
+ # @return [String] The service name.
456
+ # @return [nil] if the service should be obtained from the app config.
457
+ #
458
+ attr_accessor :service
459
+
460
+ ##
461
+ # @return [String] Path to the config file.
462
+ # @return [nil] if the default of `./app.yaml` should be used.
463
+ #
464
+ attr_accessor :config_path
465
+
466
+ ##
467
+ # @return [String] Service version of the image to use.
468
+ # @return [nil] if the most recent should be used.
469
+ #
470
+ attr_accessor :version
471
+
472
+ ##
473
+ # @return [String] The command timeout, in `1h23m45s` format.
474
+ # @return [nil] if the default of `10m` should be used.
475
+ #
476
+ attr_accessor :timeout
477
+
478
+ ##
479
+ # The command to run.
480
+ #
481
+ # @return [String] if the command is a script to be run in a shell.
482
+ # @return [Array<String>] if the command is a posix command to be run
483
+ # directly without a shell.
484
+ #
485
+ attr_accessor :command
486
+
487
+ ##
488
+ # @return [String] Custom wrapper image to use.
489
+ # @return [nil] if the default should be used.
490
+ #
491
+ attr_accessor :wrapper_image
492
+
493
+ ##
494
+ # @return [String] The execution strategy to use. Allowed values are
495
+ # `"deployment"` and `"cloud_build"`.
496
+ # @return [nil] to choose a default based on the App Engine (flexible or standard)
497
+ # or Cloud Run environments.
498
+ #
499
+ attr_accessor :strategy
500
+
501
+ ##
502
+ # @return [Symbol] The serverless product to use.
503
+ # Allowed values are {APP_ENGINE} and {CLOUD_RUN}
504
+ #
505
+ attr_accessor :product
506
+
507
+ ##
508
+ # Executes the command synchronously. Streams the logs back to standard out
509
+ # and does not return until the command has completed or timed out.
510
+
511
+ def start
512
+ resolve_parameters
513
+ case @product
514
+ when APP_ENGINE
515
+ start_app_engine
516
+ when CLOUD_RUN
517
+ start_cloud_run
518
+ end
519
+ end
520
+
521
+ def start_app_engine
522
+ app_info = version_info @service, @version
523
+ resolve_strategy app_info["env"]
524
+ if @strategy == "cloud_build"
525
+ start_build_strategy app_info
526
+ else
527
+ start_deployment_strategy app_info
528
+ end
529
+ end
530
+
531
+ def start_cloud_run
532
+ app_info = version_info_cloud_run @service
533
+ start_build_strategy app_info
534
+ end
535
+
536
+ private
537
+
538
+ ##
539
+ # @private
540
+ # Resolves and canonicalizes all the parameters.
541
+ #
542
+ def resolve_parameters
543
+ @timestamp_suffix = ::Time.now.strftime "%Y%m%d%H%M%S"
544
+ @command = ::Shellwords.split @command.to_s unless @command.is_a? Array
545
+ @project ||= default_project
546
+ @service ||= service_from_config || Exec.default_service
547
+ @timeout ||= Exec.default_timeout
548
+ @timeout_seconds = parse_timeout @timeout
549
+ @wrapper_image ||= Exec.default_wrapper_image
550
+ @product ||= default_product
551
+ if @product == APP_ENGINE
552
+ @version ||= latest_version @service
553
+ end
554
+ self
555
+ end
556
+
557
+ def resolve_strategy app_env
558
+ @strategy = @strategy.to_s.downcase
559
+ if @strategy.empty?
560
+ @strategy = app_env == "flexible" ? "cloud_build" : "deployment"
561
+ end
562
+ if app_env == "standard" && @strategy == "cloud_build" ||
563
+ @strategy != "cloud_build" && @strategy != "deployment"
564
+ raise UnsupportedStrategy.new @strategy, app_env
565
+ end
566
+ @strategy
567
+ end
568
+
569
+ def service_from_config
570
+ return nil if !@config_path && @service
571
+ @config_path ||= Exec.default_config_path
572
+ ::YAML.load_file(config_path)["service"]
573
+ rescue ::Errno::ENOENT
574
+ raise ConfigFileNotFound, @config_path
575
+ rescue ::StandardError
576
+ raise BadConfigFileFormat, @config_path
577
+ end
578
+
579
+ def default_project
580
+ result = Exec::Gcloud.execute \
581
+ ["config", "get-value", "project"],
582
+ capture: true, assert: false
583
+ result.strip!
584
+ raise NoDefaultProject if result.empty?
585
+ result
586
+ end
587
+
588
+ def default_product
589
+ File.file?("app.yaml") ? APP_ENGINE : CLOUD_RUN
590
+ end
591
+
592
+ def parse_timeout timeout_str
593
+ matched = timeout_str =~ /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/
594
+ raise BadParameter.new "timeout", timeout_str unless matched
595
+ hours = ::Regexp.last_match(1).to_i
596
+ minutes = ::Regexp.last_match(2).to_i
597
+ seconds = ::Regexp.last_match(3).to_i
598
+ hours * 3600 + minutes * 60 + seconds
599
+ end
600
+
601
+ ##
602
+ # @private
603
+ # Returns the name of the most recently created version of the given
604
+ # service.
605
+ #
606
+ # @param service [String] Name of the service.
607
+ # @return [String] Name of the most recent version.
608
+ #
609
+ def latest_version service
610
+ result = Exec::Gcloud.execute \
611
+ [
612
+ "app", "versions", "list",
613
+ "--project", @project,
614
+ "--service", service,
615
+ "--format", "get(version.id)",
616
+ "--sort-by", "~version.createTime",
617
+ "--limit", "1"
618
+ ],
619
+ capture: true, assert: false
620
+ result = result.split.first
621
+ raise NoSuchVersion, service unless result
622
+ result
623
+ end
624
+
625
+ ##
626
+ # @private
627
+ # Returns full information on the given version of the given service.
628
+ #
629
+ # @param service [String] Name of the service. If omitted, the service
630
+ # "default" is used.
631
+ # @param version [String] Name of the version. If omitted, the most
632
+ # recently deployed is used.
633
+ # @return [Hash] A collection of fields parsed from the JSON representation
634
+ # of the version
635
+ # @return [nil] if the requested version doesn't exist.
636
+ #
637
+ def version_info service, version
638
+ service ||= "default"
639
+ version ||= latest_version service
640
+ result = Exec::Gcloud.execute \
641
+ [
642
+ "app", "versions", "describe", version,
643
+ "--project", @project,
644
+ "--service", service,
645
+ "--format", "json"
646
+ ],
647
+ capture: true, assert: false
648
+ result.strip!
649
+ raise NoSuchVersion.new(service, version) if result.empty?
650
+ ::JSON.parse result
651
+ end
652
+
653
+ def version_info_cloud_run service
654
+ service ||= "default"
655
+ result = Exec::Gcloud.execute \
656
+ [
657
+ "run", "services", "describe", service,
658
+ "--format", "json"
659
+ ],
660
+ capture: true, assert: false
661
+ result.strip!
662
+ ::JSON.parse result
663
+ end
664
+
665
+ ##
666
+ # @private
667
+ # Performs exec on a GAE standard app.
668
+ #
669
+ def start_deployment_strategy app_info
670
+ describe_deployment_strategy
671
+ entrypoint_file = app_yaml_file = temp_version = nil
672
+ begin
673
+ puts "\n---------- DEPLOY COMMAND ----------"
674
+ secret = create_secret
675
+ entrypoint_file = copy_entrypoint secret
676
+ app_yaml_file = copy_app_yaml app_info, entrypoint_file
677
+ temp_version = deploy_temp_app app_yaml_file
678
+ puts "\n---------- EXECUTE COMMAND ----------"
679
+ puts "COMMAND: #{@command.inspect}\n\n"
680
+ exit_status = track_status temp_version, secret
681
+ puts "\nEXIT STATUS: #{exit_status}"
682
+ ensure
683
+ puts "\n---------- CLEANUP ----------"
684
+ ::File.unlink entrypoint_file if entrypoint_file
685
+ ::File.unlink app_yaml_file if app_yaml_file
686
+ delete_temp_version temp_version
687
+ end
688
+ end
689
+
690
+ def describe_deployment_strategy
691
+ puts "\nUsing the `deployment` strategy for serverless:exec"
692
+ puts "(i.e. deploying a temporary version of your app)"
693
+ puts "PROJECT: #{@project}"
694
+ puts "SERVICE: #{@service}"
695
+ puts "TIMEOUT: #{@timeout}"
696
+ end
697
+
698
+ def create_secret
699
+ ::SecureRandom.alphanumeric 20
700
+ end
701
+
702
+ def copy_entrypoint secret
703
+ entrypoint_template =
704
+ ::File.join(::File.dirname(::File.dirname(__dir__)),
705
+ "data", "exec_standard_entrypoint.rb.erb")
706
+ entrypoint_file = "appengine_exec_entrypoint_#{@timestamp_suffix}.rb"
707
+ erb = ::ERB.new ::File.read entrypoint_template
708
+ data = {
709
+ secret: secret.inspect, command: command.inspect
710
+ }
711
+ result = erb.result_with_hash data
712
+ ::File.open entrypoint_file, "w" do |file|
713
+ file.write result
714
+ end
715
+ entrypoint_file
716
+ end
717
+
718
+ def copy_app_yaml app_info, entrypoint_file
719
+ yaml_data = {
720
+ "runtime" => app_info["runtime"],
721
+ "service" => @service,
722
+ "entrypoint" => "ruby #{entrypoint_file}",
723
+ "env_variables" => app_info["envVariables"],
724
+ "manual_scaling" => { "instances" => 1 }
725
+ }
726
+ if app_info["env"] == "flexible"
727
+ complete_flex_app_yaml yaml_data, app_info
728
+ else
729
+ complete_standard_app_yaml yaml_data, app_info
730
+ end
731
+ app_yaml_file = "appengine_exec_config_#{@timestamp_suffix}.yaml"
732
+ ::File.open app_yaml_file, "w" do |file|
733
+ ::Psych.dump yaml_data, file
734
+ end
735
+ app_yaml_file
736
+ end
737
+
738
+ def complete_flex_app_yaml yaml_data, app_info
739
+ yaml_data["env"] = "flex"
740
+ orig_path = (app_info["betaSettings"] || {})["module_yaml_path"]
741
+ return unless orig_path
742
+ orig_yaml = ::YAML.load_file orig_path
743
+ copy_keys = ["skip_files", "resources", "network", "runtime_config",
744
+ "beta_settings"]
745
+ copy_keys.each do |key|
746
+ yaml_data[key] = orig_yaml[key] if orig_yaml[key]
747
+ end
748
+ end
749
+
750
+ def complete_standard_app_yaml yaml_data, app_info
751
+ yaml_data["instance_class"] = app_info["instanceClass"].sub(/^F/, "B")
752
+ end
753
+
754
+ def deploy_temp_app app_yaml_file
755
+ temp_version = "appengine-exec-#{@timestamp_suffix}"
756
+ Exec::Gcloud.execute [
757
+ "app", "deploy", app_yaml_file,
758
+ "--project", @project,
759
+ "--version", temp_version,
760
+ "--no-promote", "--quiet"
761
+ ]
762
+ temp_version
763
+ end
764
+
765
+ def track_status temp_version, secret
766
+ host = "#{temp_version}.#{@service}.#{@project}.appspot.com"
767
+ ::Net::HTTP.start host do |http|
768
+ outpos = errpos = 0
769
+ delay = 0.0
770
+ loop do
771
+ sleep delay
772
+ uri = URI("http://#{host}/#{secret}")
773
+ uri.query = ::URI.encode_www_form outpos: outpos, errpos: errpos
774
+ response = http.request_get uri
775
+ data = ::JSON.parse response.body
776
+ data["outlines"].each { |line| puts "[STDOUT] #{line}" }
777
+ data["errlines"].each { |line| puts "[STDERR] #{line}" }
778
+ outpos = data["outpos"]
779
+ errpos = data["errpos"]
780
+ return data["status"] if data["status"]
781
+ if data["time"] > @timeout_seconds
782
+ http.request_post "/#{secret}/kill", ""
783
+ return "timeout"
784
+ end
785
+ if data["outlines"].empty? && data["errlines"].empty?
786
+ delay += 0.1
787
+ delay = 1.0 if delay > 1.0
788
+ else
789
+ delay = 0.0
790
+ end
791
+ end
792
+ end
793
+ end
794
+
795
+ def delete_temp_version temp_version
796
+ Exec::Gcloud.execute [
797
+ "app", "versions", "delete", temp_version,
798
+ "--project", @project,
799
+ "--service", @service,
800
+ "--quiet"
801
+ ]
802
+ end
803
+
804
+ ##
805
+ # @private
806
+ # Performs exec on a GAE flexible and Cloud Run apps.
807
+ #
808
+ def start_build_strategy app_info
809
+ if @product == APP_ENGINE
810
+ env_variables = app_info["envVariables"] || {}
811
+ beta_settings = app_info["betaSettings"] || {}
812
+ cloud_sql_instances = beta_settings["cloud_sql_instances"] || []
813
+ container = app_info["deployment"]["container"]
814
+ image = container ? container["image"] : image_from_build(app_info)
815
+ else
816
+ env_variables = {}
817
+ app_env = app_info["spec"]["template"]["spec"]["containers"][0]["env"]
818
+ app_env&.each { |env| env_variables[env["name"]] = env["value"] }
819
+ metadata_annotations = app_info["spec"]["template"]["metadata"]["annotations"]
820
+ cloud_sql_instances = metadata_annotations["run.googleapis.com/cloudsql-instances"] || []
821
+ image = metadata_annotations["client.knative.dev/user-image"]
822
+ end
823
+
824
+ describe_build_strategy
825
+
826
+ config = build_config command, image, env_variables, cloud_sql_instances
827
+ file = ::Tempfile.new ["cloudbuild_", ".json"]
828
+ begin
829
+ ::JSON.dump config, file
830
+ file.flush
831
+ execute_command = [
832
+ "builds", "submit",
833
+ "--project", @project,
834
+ "--no-source",
835
+ "--config", file.path,
836
+ "--timeout", @timeout
837
+ ]
838
+ execute_command.concat ["--gcs-log-dir", @gcs_log_dir] unless @gcs_log_dir.nil?
839
+ Exec::Gcloud.execute execute_command
840
+ ensure
841
+ file.close!
842
+ end
843
+ end
844
+
845
+ ##
846
+ # @private
847
+ # Workaround for https://github.com/GoogleCloudPlatform/appengine-ruby/issues/33
848
+ # Determines the image by looking it up in Cloud Build
849
+ #
850
+ def image_from_build app_info
851
+ create_time = ::DateTime.parse(app_info["createTime"]).to_time.utc
852
+ after_time = (create_time - 3600).strftime "%Y-%m-%dT%H:%M:%SZ"
853
+ before_time = (create_time + 3600).strftime "%Y-%m-%dT%H:%M:%SZ"
854
+ partial_uri = "gcr.io/#{@project}/appengine/#{@service}.#{@version}"
855
+ filter = "createTime>#{after_time} createTime<#{before_time} images[]:#{partial_uri}"
856
+ result = Exec::Gcloud.execute \
857
+ [
858
+ "builds", "list",
859
+ "--project", @project,
860
+ "--filter", filter,
861
+ "--format", "json"
862
+ ],
863
+ capture: true, assert: false
864
+ result.strip!
865
+ raise NoSuchVersion.new(@service, @version) if result.empty?
866
+ build_info = ::JSON.parse(result).first
867
+ build_info["images"].first
868
+ end
869
+
870
+ def describe_build_strategy
871
+ puts "\nUsing the `cloud_build` strategy for serverless:exec"
872
+ puts "(i.e. running your app image in Cloud Build)"
873
+ puts "PROJECT: #{@project}"
874
+ puts "SERVICE: #{@service}"
875
+ puts "VERSION: #{@version}"
876
+ puts "TIMEOUT: #{@timeout}"
877
+ puts ""
878
+ end
879
+
880
+ ##
881
+ # @private
882
+ # Builds a cloudbuild config as a data structure.
883
+ #
884
+ # @param command [Array<String>] The command in array form.
885
+ # @param image [String] The fully qualified image path.
886
+ # @param env_variables [Hash<String,String>] Environment variables.
887
+ # @param cloud_sql_instances [String,Array<String>] Names of cloud sql
888
+ # instances to connect to.
889
+ #
890
+ def build_config command, image, env_variables, cloud_sql_instances
891
+ args = ["-i", image]
892
+ env_variables.each do |k, v|
893
+ v = v.gsub "$", "$$"
894
+ args << "-e" << "#{k}=#{v}"
895
+ end
896
+ unless cloud_sql_instances.empty?
897
+ cloud_sql_instances = Array(cloud_sql_instances)
898
+ cloud_sql_instances.each do |sql|
899
+ args << "-s" << sql
900
+ end
901
+ end
902
+ args << "--"
903
+ args += command
904
+
905
+ {
906
+ "steps" => [
907
+ "name" => @wrapper_image,
908
+ "args" => args
909
+ ]
910
+ }
911
+ end
912
+ end
913
+ end
914
+ end