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 +7 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +27 -0
- data/LICENSE +202 -0
- data/README.md +49 -0
- data/lib/google/serverless/exec.rb +914 -0
- data/lib/google/serverless/exec/gcloud.rb +200 -0
- data/lib/google/serverless/exec/tasks.rb +381 -0
- data/lib/google/serverless/exec/version.rb +24 -0
- metadata +199 -0
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
data/CHANGELOG.md
ADDED
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
|