google-serverless-exec 0.1.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 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