cloud-mu 3.1.1 → 3.1.2beta2
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 +4 -4
- data/Berksfile.lock +179 -0
- data/bin/mu-adopt +1 -1
- data/bin/mu-azure-tests +46 -0
- data/bin/mu-cleanup +3 -1
- data/bin/mu-configure +7 -0
- data/cloud-mu.gemspec +4 -3
- data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
- data/extras/clean-stock-amis +0 -0
- data/extras/generate-stock-images +0 -0
- data/extras/list-stock-amis +0 -0
- data/extras/vault_tools/export_vaults.sh +0 -0
- data/extras/vault_tools/recreate_vaults.sh +0 -0
- data/extras/vault_tools/test_vaults.sh +0 -0
- data/modules/mu/cleanup.rb +15 -3
- data/modules/mu/clouds/aws/bucket.rb +6 -0
- data/modules/mu/clouds/aws/endpoint.rb +2 -0
- data/modules/mu/clouds/aws/firewall_rule.rb +31 -0
- data/modules/mu/clouds/aws/function.rb +45 -2
- data/modules/mu/clouds/aws/loadbalancer.rb +3 -1
- data/modules/mu/clouds/aws/role.rb +3 -1
- data/modules/mu/clouds/aws/server.rb +10 -4
- data/modules/mu/clouds/aws/server_pool.rb +4 -3
- data/modules/mu/clouds/aws/vpc.rb +28 -3
- data/modules/mu/clouds/google/function.rb +679 -0
- data/modules/mu/clouds/google/vpc.rb +1 -1
- data/modules/mu/clouds/google.rb +54 -9
- data/modules/mu/config/function.rb +12 -40
- data/modules/mu/config.rb +2 -1
- data/modules/mu/groomers/ansible.rb +7 -5
- data/modules/mu/kittens.rb +22134 -0
- data/modules/mu/mu.yaml.rb +282 -0
- data/modules/mu.rb +4 -1
- metadata +41 -20
@@ -0,0 +1,679 @@
|
|
1
|
+
# Copyright:: Copyright (c) 2020 eGlobalTech, Inc., all rights reserved
|
2
|
+
#
|
3
|
+
# Licensed under the BSD-3 license (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License in the root of the project or at
|
6
|
+
#
|
7
|
+
# http://egt-labs.com/mu/LICENSE.html
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
module MU
|
16
|
+
class Cloud
|
17
|
+
class Google
|
18
|
+
# Creates a Google Cloud Function as configured in {MU::Config::BasketofKittens::functions}
|
19
|
+
class Function < MU::Cloud::Function
|
20
|
+
|
21
|
+
require 'zip'
|
22
|
+
require 'tmpdir'
|
23
|
+
|
24
|
+
# Known-good code blobs to upload when initially creating functions
|
25
|
+
HELLO_WORLDS = {
|
26
|
+
"nodejs" => {
|
27
|
+
"index.js" => %Q{
|
28
|
+
/**
|
29
|
+
* Responds to any HTTP request.
|
30
|
+
*
|
31
|
+
* @param {!express:Request} req HTTP request context.
|
32
|
+
* @param {!express:Response} res HTTP response context.
|
33
|
+
*/
|
34
|
+
exports.hello_world = (req, res) => {
|
35
|
+
let message = req.query.message || req.body.message || 'Hello World!';
|
36
|
+
res.status(200).send(message);
|
37
|
+
};
|
38
|
+
},
|
39
|
+
"package.json" => %Q{
|
40
|
+
{
|
41
|
+
"name": "sample-http",
|
42
|
+
"version": "0.0.1"
|
43
|
+
}
|
44
|
+
}
|
45
|
+
},
|
46
|
+
"python" => {
|
47
|
+
"main.py" => %Q{
|
48
|
+
def hello_world(request):
|
49
|
+
"""Responds to any HTTP request.
|
50
|
+
Args:
|
51
|
+
request (flask.Request): HTTP request object.
|
52
|
+
Returns:
|
53
|
+
The response text or any set of values that can be turned into a
|
54
|
+
Response object using
|
55
|
+
`make_response <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>`.
|
56
|
+
"""
|
57
|
+
request_json = request.get_json()
|
58
|
+
if request.args and 'message' in request.args:
|
59
|
+
return request.args.get('message')
|
60
|
+
elif request_json and 'message' in request_json:
|
61
|
+
return request_json['message']
|
62
|
+
else:
|
63
|
+
return f'Hello World!'
|
64
|
+
},
|
65
|
+
"requirements.txt" => "# put your modules here\n"
|
66
|
+
},
|
67
|
+
"go" => {
|
68
|
+
"function.go" => %Q{
|
69
|
+
// Package p contains an HTTP Cloud Function.
|
70
|
+
package p
|
71
|
+
|
72
|
+
import (
|
73
|
+
"encoding/json"
|
74
|
+
"fmt"
|
75
|
+
"html"
|
76
|
+
"net/http"
|
77
|
+
)
|
78
|
+
|
79
|
+
// HelloWorld prints the JSON encoded "message" field in the body
|
80
|
+
// of the request or "Hello, World!" if there isn't one.
|
81
|
+
func hello_world(w http.ResponseWriter, r *http.Request) {
|
82
|
+
var d struct {
|
83
|
+
Message string `json:"message"`
|
84
|
+
}
|
85
|
+
if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
|
86
|
+
fmt.Fprint(w, "Hello World!")
|
87
|
+
return
|
88
|
+
}
|
89
|
+
if d.Message == "" {
|
90
|
+
fmt.Fprint(w, "Hello World!")
|
91
|
+
return
|
92
|
+
}
|
93
|
+
fmt.Fprint(w, html.EscapeString(d.Message))
|
94
|
+
}
|
95
|
+
},
|
96
|
+
"go.mod" => %Q{
|
97
|
+
module example.com/cloudfunction
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
# Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like <tt>@vpc</tt>, for us.
|
103
|
+
# @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
|
104
|
+
def initialize(**args)
|
105
|
+
super
|
106
|
+
@mu_name ||= @deploy.getResourceName(@config['name'])
|
107
|
+
end
|
108
|
+
|
109
|
+
# Called automatically by {MU::Deploy#createResources}
|
110
|
+
def create
|
111
|
+
labels = Hash[@tags.keys.map { |k|
|
112
|
+
[k.downcase, @tags[k].downcase.gsub(/[^-_a-z0-9]/, '-')] }
|
113
|
+
]
|
114
|
+
labels["name"] = MU::Cloud::Google.nameStr(@mu_name)
|
115
|
+
|
116
|
+
location = "projects/"+@config['project']+"/locations/"+@config['region']
|
117
|
+
sa = nil
|
118
|
+
retries = 0
|
119
|
+
begin
|
120
|
+
sa_ref = MU::Config::Ref.get(@config['service_account'])
|
121
|
+
sa = @deploy.findLitterMate(name: sa_ref.name, type: "users")
|
122
|
+
if !sa or !sa.cloud_desc
|
123
|
+
sleep 10
|
124
|
+
end
|
125
|
+
rescue ::Google::Apis::ClientError => e
|
126
|
+
if e.message.match(/notFound:/)
|
127
|
+
sleep 10
|
128
|
+
retries += 1
|
129
|
+
retry
|
130
|
+
end
|
131
|
+
end while !sa or !sa.cloud_desc and retries < 5
|
132
|
+
|
133
|
+
if !sa or !sa.cloud_desc
|
134
|
+
raise MuError, "Failed to get service account cloud id from #{@config['service_account'].to_s}"
|
135
|
+
end
|
136
|
+
|
137
|
+
desc = {
|
138
|
+
name: location+"/functions/"+@mu_name.downcase,
|
139
|
+
runtime: @config['runtime'],
|
140
|
+
timeout: @config['timeout'].to_s+"s",
|
141
|
+
# entry_point: "hello_world",
|
142
|
+
entry_point: @config['handler'],
|
143
|
+
description: @deploy.deploy_id,
|
144
|
+
service_account_email: sa.cloud_desc.email,
|
145
|
+
labels: labels,
|
146
|
+
available_memory_mb: @config['memory']
|
147
|
+
}
|
148
|
+
|
149
|
+
# XXX This network argument is deprecated in favor of using VPC
|
150
|
+
# Connectors. Which would be fine, except there's no API support for
|
151
|
+
# interacting with VPC Connectors. Can't create them, can't list them,
|
152
|
+
# can't do anything except pass their ids into Cloud Functions or
|
153
|
+
# AppEngine and hope for the best.
|
154
|
+
if @config['vpc_connector']
|
155
|
+
desc[:vpc_connector] = @config['vpc_connector']
|
156
|
+
elsif @vpc
|
157
|
+
desc[:network] = @vpc.url.sub(/^.*?\/projects\//, 'projects/')
|
158
|
+
end
|
159
|
+
|
160
|
+
if @config['triggers']
|
161
|
+
desc[:event_trigger] = MU::Cloud::Google.function(:EventTrigger).new(
|
162
|
+
event_type: @config['triggers'].first['event'],
|
163
|
+
resource: @config['triggers'].first['resource']
|
164
|
+
)
|
165
|
+
else
|
166
|
+
desc[:https_trigger] = MU::Cloud::Google.function(:HttpsTrigger).new
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
if @config['environment_variable']
|
171
|
+
@config['environment_variable'].each { |var|
|
172
|
+
desc[:environment_variables] ||= {}
|
173
|
+
desc[:environment_variables][var["key"].to_s] = var["value"].to_s
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
# hello_code = nil
|
178
|
+
# HELLO_WORLDS.each_pair { |runtime, code|
|
179
|
+
# if @config['runtime'].match(/^#{Regexp.quote(runtime)}/)
|
180
|
+
# hello_code = code
|
181
|
+
# break
|
182
|
+
# end
|
183
|
+
# }
|
184
|
+
if @config['code']['gs_url']
|
185
|
+
desc[:source_archive_url] = @config['code']['gs_url']
|
186
|
+
elsif @config['code']['zip_file']
|
187
|
+
desc[:source_archive_url] = MU::Cloud::Google::Function.uploadPackage(@config['code']['zip_file'], @mu_name+"-cloudfunction.zip", credentials: @credentials)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Dir.mktmpdir(@mu_name) { |dir|
|
191
|
+
# hello_code.each_pair { |file, contents|
|
192
|
+
# f = File.open(dir+"/"+file, "w")
|
193
|
+
# f.puts contents
|
194
|
+
# f.close
|
195
|
+
# Zip::File.open(dir+"/function.zip", Zip::File::CREATE) { |z|
|
196
|
+
# z.add(file, dir+"/"+file)
|
197
|
+
# }
|
198
|
+
# }
|
199
|
+
# desc[:source_archive_url] = MU::Cloud::Google::Function.uploadPackage(dir+"/function.zip", @mu_name+"-cloudfunction.zip", credentials: @credentials)
|
200
|
+
# }
|
201
|
+
|
202
|
+
func_obj = MU::Cloud::Google.function(:CloudFunction).new(desc)
|
203
|
+
MU.log "Creating Cloud Function #{@mu_name} in #{location}", details: func_obj
|
204
|
+
resp = MU::Cloud::Google.function(credentials: @credentials).create_project_location_function(location, func_obj)
|
205
|
+
@cloud_id = resp.name
|
206
|
+
end
|
207
|
+
|
208
|
+
# Called automatically by {MU::Deploy#createResources}
|
209
|
+
def groom
|
210
|
+
desc = {}
|
211
|
+
labels = Hash[@tags.keys.map { |k|
|
212
|
+
[k.downcase, @tags[k].downcase.gsub(/[^-_a-z0-9]/, '-')] }
|
213
|
+
]
|
214
|
+
labels["name"] = MU::Cloud::Google.nameStr(@mu_name)
|
215
|
+
|
216
|
+
if cloud_desc.labels != labels
|
217
|
+
desc[:labels] = labels
|
218
|
+
end
|
219
|
+
|
220
|
+
if cloud_desc.runtime != @config['runtime']
|
221
|
+
desc[:runtime] = @config['runtime']
|
222
|
+
end
|
223
|
+
if cloud_desc.timeout != @config['timeout'].to_s+"s"
|
224
|
+
desc[:timeout] = @config['timeout'].to_s+"s"
|
225
|
+
end
|
226
|
+
if cloud_desc.entry_point != @config['handler']
|
227
|
+
desc[:entry_point] = @config['handler']
|
228
|
+
end
|
229
|
+
if cloud_desc.available_memory_mb != @config['memory']
|
230
|
+
desc[:available_memory_mb] = @config['memory']
|
231
|
+
end
|
232
|
+
if @config['environment_variable']
|
233
|
+
@config['environment_variable'].each { |var|
|
234
|
+
if !cloud_desc.environment_variables or
|
235
|
+
cloud_desc.environment_variables[var["key"].to_s] != var["value"].to_s
|
236
|
+
desc[:environment_variables] ||= {}
|
237
|
+
desc[:environment_variables][var["key"].to_s] = var["value"].to_s
|
238
|
+
end
|
239
|
+
}
|
240
|
+
end
|
241
|
+
if @config['triggers']
|
242
|
+
if !cloud_desc.event_trigger or
|
243
|
+
cloud_desc.event_trigger.event_type != @config['triggers'].first['event'] or
|
244
|
+
cloud_desc.event_trigger.resource != @config['triggers'].first['resource']
|
245
|
+
desc[:event_trigger] = MU::Cloud::Google.function(:EventTrigger).new(
|
246
|
+
event_type: @config['triggers'].first['event'],
|
247
|
+
resource: @config['triggers'].first['resource']
|
248
|
+
)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
current = Dir.mktmpdir(@mu_name+"-current") { |dir|
|
253
|
+
MU::Cloud::Google::Function.downloadPackage(@cloud_id, dir+"/current.zip", credentials: @credentials)
|
254
|
+
File.read("#{dir}/current.zip")
|
255
|
+
}
|
256
|
+
|
257
|
+
source_url = nil
|
258
|
+
new = if @config['code']['zip_file']
|
259
|
+
File.read(@config['code']['zip_file'])
|
260
|
+
elsif @config['code']['gs_url']
|
261
|
+
@config['code']['gs_url'].match(/^gs:\/\/([^\/]+)\/(.*)/)
|
262
|
+
bucket = Regexp.last_match[1]
|
263
|
+
path = Regexp.last_match[2]
|
264
|
+
Dir.mktmpdir(@mu_name+"-new") { |dir|
|
265
|
+
MU::Cloud::Google.storage(credentials: @credentials).get_object(bucket, path, download_dest: dir+"/new.zip")
|
266
|
+
File.read(dir+"/new.zip")
|
267
|
+
}
|
268
|
+
end
|
269
|
+
if @config['code']['gs_url'] and
|
270
|
+
(@config['code']['gs_url'] != cloud_desc.source_archive_url or
|
271
|
+
current != new)
|
272
|
+
desc[:source_archive_url] = @config['code']['gs_url']
|
273
|
+
elsif @config['code']['zip_file'] and current != new
|
274
|
+
desc[:source_archive_url] = MU::Cloud::Google::Function.uploadPackage(@config['code']['zip_file'], @mu_name+"-cloudfunction.zip", credentials: @credentials)
|
275
|
+
end
|
276
|
+
|
277
|
+
if desc.size > 0
|
278
|
+
# A trigger and some code must always be specified, apparently. The
|
279
|
+
# endpoint hangs indefinitely if either is missing. Charming.
|
280
|
+
if !desc[:https_trigger] and !desc[:event_trigger]
|
281
|
+
if cloud_desc.https_trigger
|
282
|
+
desc[:https_trigger] = MU::Cloud::Google.function(:HttpsTrigger).new
|
283
|
+
else
|
284
|
+
desc[:event_trigger] = cloud_desc.event_trigger
|
285
|
+
end
|
286
|
+
end
|
287
|
+
if !desc[:source_archive_url] and !desc[:source_upload_url]
|
288
|
+
if cloud_desc.source_archive_url
|
289
|
+
desc[:source_archive_url] = cloud_desc.source_archive_url
|
290
|
+
else
|
291
|
+
desc[:source_upload_url] = cloud_desc.source_upload_url
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
func_obj = MU::Cloud::Google.function(:CloudFunction).new(desc)
|
296
|
+
MU.log "Updating Cloud Function #{@mu_name}", MU::NOTICE, details: func_obj
|
297
|
+
begin
|
298
|
+
MU::Cloud::Google.function(credentials: @credentials).patch_project_location_function(
|
299
|
+
@cloud_id,
|
300
|
+
func_obj
|
301
|
+
)
|
302
|
+
rescue ::Google::Apis::ClientError => e
|
303
|
+
MU.log "Error updating Cloud Function #{@mu_name}.", MU::ERR
|
304
|
+
if desc[:source_archive_url]
|
305
|
+
main_file = nil
|
306
|
+
HELLO_WORLDS.each_pair { |runtime, code|
|
307
|
+
if @config['runtime'].match(/^#{Regexp.quote(runtime)}/)
|
308
|
+
main_file = code.keys.first
|
309
|
+
break
|
310
|
+
end
|
311
|
+
}
|
312
|
+
MU.log "Verify that the specified code is compatible with the #{@config['runtime']} runtime and has an entry point named #{@config['handler']} in #{main_file}", MU::ERR, details: @config['code']
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# service_account_email: sa.kitten.cloud_desc.email,
|
318
|
+
# labels: labels,
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
# Return the metadata for this project's configuration
|
323
|
+
# @return [Hash]
|
324
|
+
def notify
|
325
|
+
MU.structToHash(cloud_desc)
|
326
|
+
end
|
327
|
+
|
328
|
+
# Does this resource type exist as a global (cloud-wide) artifact, or
|
329
|
+
# is it localized to a region/zone?
|
330
|
+
# @return [Boolean]
|
331
|
+
def self.isGlobal?
|
332
|
+
false
|
333
|
+
end
|
334
|
+
|
335
|
+
# Denote whether this resource implementation is experiment, ready for
|
336
|
+
# testing, or ready for production use.
|
337
|
+
def self.quality
|
338
|
+
MU::Cloud::BETA
|
339
|
+
end
|
340
|
+
|
341
|
+
# Remove all Google projects associated with the currently loaded deployment. Try to, anyway.
|
342
|
+
# @param noop [Boolean]: If true, will only print what would be done
|
343
|
+
# @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
|
344
|
+
# @param region [String]: The cloud provider region
|
345
|
+
# @return [void]
|
346
|
+
def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
|
347
|
+
flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
|
348
|
+
return if !MU::Cloud::Google::Habitat.isLive?(flags["project"], credentials)
|
349
|
+
# Make sure we catch regional *and* zone functions
|
350
|
+
found = MU::Cloud::Google::Function.find(credentials: credentials, region: region, project: flags["project"])
|
351
|
+
found.each_pair { |cloud_id, desc|
|
352
|
+
if (desc.description and desc.description == MU.deploy_id) or
|
353
|
+
(desc.labels and desc.labels["mu-id"] == MU.deploy_id.downcase) or
|
354
|
+
(flags["known"] and flags["known"].include?(cloud_id))
|
355
|
+
MU.log "Deleting Cloud Function #{desc.name}"
|
356
|
+
if !noop
|
357
|
+
MU::Cloud::Google.function(credentials: credentials).delete_project_location_function(cloud_id)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
}
|
361
|
+
|
362
|
+
end
|
363
|
+
|
364
|
+
# Locate an existing project
|
365
|
+
# @return [Hash<OpenStruct>]: The cloud provider's complete descriptions of matching project
|
366
|
+
def self.find(**args)
|
367
|
+
args[:project] ||= args[:habitat]
|
368
|
+
args[:project] ||= MU::Cloud::Google.defaultProject(args[:credentials])
|
369
|
+
location = args[:region] || args[:availability_zone] || "-"
|
370
|
+
|
371
|
+
found = {}
|
372
|
+
|
373
|
+
if args[:cloud_id]
|
374
|
+
resp = begin
|
375
|
+
MU::Cloud::Google.function(credentials: args[:credentials]).get_project_location_function(args[:cloud_id])
|
376
|
+
rescue ::Google::Apis::ClientError => e
|
377
|
+
raise e if !e.message.match(/forbidden:/)
|
378
|
+
end
|
379
|
+
found[args[:cloud_id]] = resp if resp
|
380
|
+
else
|
381
|
+
resp = begin
|
382
|
+
MU::Cloud::Google.function(credentials: args[:credentials]).list_project_location_functions("projects/#{args[:project]}/locations/#{location}")
|
383
|
+
rescue ::Google::Apis::ClientError => e
|
384
|
+
raise e if !e.message.match(/forbidden:/)
|
385
|
+
end
|
386
|
+
|
387
|
+
if resp and resp.functions and !resp.functions.empty?
|
388
|
+
resp.functions.each { |f|
|
389
|
+
found[f.name.sub(/.*?\/projects\//, 'projects/')] = f
|
390
|
+
}
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
found
|
395
|
+
end
|
396
|
+
|
397
|
+
# Reverse-map our cloud description into a runnable config hash.
|
398
|
+
# We assume that any values we have in +@config+ are placeholders, and
|
399
|
+
# calculate our own accordingly based on what's live in the cloud.
|
400
|
+
def toKitten(rootparent: nil, billing: nil, habitats: nil)
|
401
|
+
bok = {
|
402
|
+
"cloud" => "Google",
|
403
|
+
"cloud_id" => @cloud_id,
|
404
|
+
"credentials" => @credentials,
|
405
|
+
"project" => @project
|
406
|
+
}
|
407
|
+
|
408
|
+
@cloud_id.match(/^projects\/([^\/]+)\/locations\/([^\/]+)\/functions\/(.*)/)
|
409
|
+
bok["project"] ||= Regexp.last_match[1]
|
410
|
+
bok["region"] = Regexp.last_match[2]
|
411
|
+
bok["name"] = Regexp.last_match[3]
|
412
|
+
bok["runtime"] = cloud_desc.runtime
|
413
|
+
bok["memory"] = cloud_desc.available_memory_mb
|
414
|
+
bok["handler"] = cloud_desc.entry_point
|
415
|
+
bok["timeout"] = cloud_desc.timeout.gsub(/[^\d]/, '').to_i
|
416
|
+
|
417
|
+
if cloud_desc.vpc_connector
|
418
|
+
bok["vpc_connector"] = cloud_desc.vpc_connector
|
419
|
+
elsif cloud_desc.network
|
420
|
+
cloud_desc.network.match(/^projects\/(.*?)\/.*?\/networks\/([^\/]+)(?:$|\/)/)
|
421
|
+
vpc_proj = Regexp.last_match[1]
|
422
|
+
vpc_id = Regexp.last_match[2]
|
423
|
+
|
424
|
+
bok['vpc'] = MU::Config::Ref.get(
|
425
|
+
id: vpc_id,
|
426
|
+
cloud: "Google",
|
427
|
+
habitat: MU::Config::Ref.get(
|
428
|
+
id: vpc_proj,
|
429
|
+
cloud: "Google",
|
430
|
+
credentials: @credentials,
|
431
|
+
type: "habitats"
|
432
|
+
),
|
433
|
+
credentials: @credentials,
|
434
|
+
type: "vpcs"
|
435
|
+
)
|
436
|
+
end
|
437
|
+
|
438
|
+
if cloud_desc.environment_variables and cloud_desc.environment_variables.size > 0
|
439
|
+
bok['environment_variable'] = cloud_desc.environment_variables.keys.map { |k| { "key" => k, "value" => cloud_desc.environment_variables[k] } }
|
440
|
+
end
|
441
|
+
if cloud_desc.labels and cloud_desc.labels.size > 0
|
442
|
+
bok['tags'] = cloud_desc.labels.keys.map { |k| { "key" => k, "value" => cloud_desc.labels[k] } }
|
443
|
+
end
|
444
|
+
|
445
|
+
if cloud_desc.event_trigger
|
446
|
+
bok['triggers'] = [
|
447
|
+
{
|
448
|
+
"event" => cloud_desc.event_trigger.event_type,
|
449
|
+
"resource" => cloud_desc.event_trigger.resource
|
450
|
+
}
|
451
|
+
]
|
452
|
+
end
|
453
|
+
|
454
|
+
codefile = bok["project"]+"_"+bok["region"]+"_"+bok["name"]+".zip"
|
455
|
+
MU::Cloud::Google::Function.downloadPackage(@cloud_id, codefile, credentials: @config['credentials'])
|
456
|
+
bok['code'] = {
|
457
|
+
'zip_file' => codefile
|
458
|
+
}
|
459
|
+
|
460
|
+
bok
|
461
|
+
end
|
462
|
+
|
463
|
+
|
464
|
+
# Cloud-specific configuration properties.
|
465
|
+
# @param config [MU::Config]: The calling MU::Config object
|
466
|
+
# @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
|
467
|
+
def self.schema(config)
|
468
|
+
toplevel_required = ["runtime"]
|
469
|
+
schema = {
|
470
|
+
"triggers" => {
|
471
|
+
"type" => "array",
|
472
|
+
"items" => {
|
473
|
+
"type" => "object",
|
474
|
+
"description" => "Trigger for Cloud Function",
|
475
|
+
"required" => ["event", "resource"],
|
476
|
+
"additionalProperties" => false,
|
477
|
+
"properties" => {
|
478
|
+
"event" => {
|
479
|
+
"type" => "string",
|
480
|
+
"description" => "The type of event to observe, such as +providers/cloud.storage/eventTypes/object.change+ or +providers/cloud.pubsub/eventTypes/topic.publish+. Event types match pattern +providers//eventTypes/.*+"
|
481
|
+
},
|
482
|
+
"resource" => {
|
483
|
+
"type" => "string",
|
484
|
+
"description" => "The resource(s) from which to observe events, for example, +projects/_/buckets/myBucket+. Not all syntactically correct values are accepted by all services."
|
485
|
+
}
|
486
|
+
}
|
487
|
+
}
|
488
|
+
},
|
489
|
+
"service_account" => MU::Cloud::Google::Server.schema(config)[1]["service_account"],
|
490
|
+
"runtime" => {
|
491
|
+
"type" => "string",
|
492
|
+
"enum" => %w{nodejs go python nodejs8 nodejs10 python37 go111 go113},
|
493
|
+
},
|
494
|
+
"vpc_connector" => {
|
495
|
+
"type" => "string",
|
496
|
+
"description" => "+DEPRECATED+ VPC Connector to attach, of the form +projects/my-project/locations/some-region/connectors/my-connector+. This option will be removed once proper google-cloud-sdk support for VPC Connectors becomes available, at which point we will piggyback on the normal +vpc+ stanza and resolve connectors as needed."
|
497
|
+
},
|
498
|
+
"code" => {
|
499
|
+
"type" => "object",
|
500
|
+
"properties" => {
|
501
|
+
"gs_url" => {
|
502
|
+
"type" => "string",
|
503
|
+
"description" => "A Google Cloud Storage URL, starting with gs://, pointing to the zip archive which contains the function."
|
504
|
+
}
|
505
|
+
}
|
506
|
+
}
|
507
|
+
}
|
508
|
+
[toplevel_required, schema]
|
509
|
+
end
|
510
|
+
|
511
|
+
# Upload a zipfile to our admin Cloud Storage bucket, for use by
|
512
|
+
# Cloud Functions
|
513
|
+
# @param function_id [String]: The cloud_id of the Function, in the format ++
|
514
|
+
# @param zipfile [String]: Target filename
|
515
|
+
# @param credentials [String]
|
516
|
+
def self.downloadPackage(function_id, zipfile, credentials: nil)
|
517
|
+
cloud_desc = MU::Cloud::Google::Function.find(cloud_id: function_id, credentials: credentials).values.first
|
518
|
+
if !cloud_desc
|
519
|
+
raise MuError, "Couldn't find Cloud Function #{function_id}"
|
520
|
+
end
|
521
|
+
|
522
|
+
if cloud_desc.source_archive_url
|
523
|
+
cloud_desc.source_archive_url.match(/^gs:\/\/([^\/]+)\/(.*)/)
|
524
|
+
bucket = Regexp.last_match[1]
|
525
|
+
path = Regexp.last_match[2]
|
526
|
+
current = nil
|
527
|
+
MU::Cloud::Google.storage(credentials: credentials).get_object(bucket, path, download_dest: zipfile)
|
528
|
+
elsif cloud_desc.source_upload_url
|
529
|
+
resp = MU::Cloud::Google.function(credentials: credentials).generate_function_download_url(
|
530
|
+
function_id
|
531
|
+
)
|
532
|
+
if resp and resp.download_url
|
533
|
+
f = File.open(zipfile, "wb")
|
534
|
+
f.write Net::HTTP.get(URI(resp.download_url))
|
535
|
+
f.close
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# Upload a zipfile to our admin Cloud Storage bucket, for use by
|
541
|
+
# Cloud Functions
|
542
|
+
# @param zipfile [String]: Source file
|
543
|
+
# @param filename [String]: Target filename
|
544
|
+
# @param credentials [String]
|
545
|
+
# @return [String]: The Cloud Storage URL to the result
|
546
|
+
def self.uploadPackage(zipfile, filename, credentials: nil)
|
547
|
+
bucket = MU::Cloud::Google.adminBucketName(credentials)
|
548
|
+
obj_obj = MU::Cloud::Google.storage(:Object).new(
|
549
|
+
content_type: "application/zip",
|
550
|
+
name: filename
|
551
|
+
)
|
552
|
+
MU::Cloud::Google.storage(credentials: credentials).insert_object(
|
553
|
+
bucket,
|
554
|
+
obj_obj,
|
555
|
+
upload_source: zipfile
|
556
|
+
)
|
557
|
+
return "gs://#{bucket}/#{filename}"
|
558
|
+
# XXX this is the canonical, correct way to do this, but it doesn't work.
|
559
|
+
# "Anonymous caller does not have storage.objects.create access to gcf-upload-us-east4-9068f7a1-7c08-4daa-8b83-d26e098e9c44/bcddc43c-f74d-46c0-bfdd-c215829a23f2.zip."
|
560
|
+
#
|
561
|
+
# upload_obj = MU::Cloud::Google.function(credentials: credentials).generate_function_upload_url(location, MU::Cloud::Google.function(:GenerateUploadUrlRequest).new)
|
562
|
+
# MU.log "Uploading #{zipfile} to #{upload_obj.upload_url}"
|
563
|
+
# uri = URI(upload_obj.upload_url)
|
564
|
+
# req = Net::HTTP::Put.new(uri.path, { "Content-Type" => "application/zip", "x-goog-content-length-range" => "0,104857600"})
|
565
|
+
#req["Content-Length"] = nil
|
566
|
+
# req.body = File.read(zipfile, mode: "rb")
|
567
|
+
# pp req
|
568
|
+
# http = Net::HTTP.new(uri.hostname, uri.port)
|
569
|
+
# http.set_debug_output($stdout)
|
570
|
+
# resp = http.request(req)
|
571
|
+
# upload_obj.upload_url
|
572
|
+
end
|
573
|
+
|
574
|
+
# Cloud-specific pre-processing of {MU::Config::BasketofKittens::functions}, bare and unvalidated.
|
575
|
+
# @param function [Hash]: The resource to process and validate
|
576
|
+
# @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
|
577
|
+
# @return [Boolean]: True if validation succeeded, False otherwise
|
578
|
+
def self.validateConfig(function, configurator)
|
579
|
+
ok = true
|
580
|
+
function['project'] ||= MU::Cloud::Google.defaultProject(function['credentials'])
|
581
|
+
function['region'] ||= MU::Cloud::Google.myRegion(function['credentials'])
|
582
|
+
if function['runtime'] == "python"
|
583
|
+
function['runtime'] = "python37"
|
584
|
+
elsif function['runtime'] == "go"
|
585
|
+
function['runtime'] = "go113"
|
586
|
+
elsif function['runtime'] == "nodejs"
|
587
|
+
function['runtime'] = "nodejs8"
|
588
|
+
end
|
589
|
+
# XXX list_project_locations
|
590
|
+
|
591
|
+
if !function['code'] or (!function['code']['zip_file'] and !function['code']['gs_url'])
|
592
|
+
MU.log "Must specify a code source in Cloud Function #{function['name']}", MU::ERR
|
593
|
+
ok = false
|
594
|
+
elsif function['code']['zip_file']
|
595
|
+
z = Zip::File.open(function['code']['zip_file'])
|
596
|
+
if function['runtime'].match(/^python/)
|
597
|
+
begin
|
598
|
+
z.get_entry("main.py")
|
599
|
+
rescue Errno::ENOENT
|
600
|
+
MU.log function['code']['zip_file']+" does not contain main.py, required for runtime #{function['runtime']}", MU::ERR
|
601
|
+
ok = false
|
602
|
+
end
|
603
|
+
elsif function['runtime'].match(/^nodejs/)
|
604
|
+
begin
|
605
|
+
z.get_entry("index.js")
|
606
|
+
rescue Errno::ENOENT
|
607
|
+
begin
|
608
|
+
z.get_entry("function.js")
|
609
|
+
rescue
|
610
|
+
MU.log function['code']['zip_file']+" does not contain function.js or index.js, at least one must be present for runtime #{function['runtime']}", MU::ERR
|
611
|
+
ok = false
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
if function['service_account']
|
618
|
+
if !function['service_account'].is_a?(MU::Config::Ref)
|
619
|
+
function['service_account']['cloud'] = "Google"
|
620
|
+
function['service_account']['habitat'] ||= function['project']
|
621
|
+
end
|
622
|
+
found = MU::Config::Ref.get(function['service_account'])
|
623
|
+
if found.id and !found.kitten
|
624
|
+
MU.log "Cloud Function #{function['name']} failed to locate service account #{function['service_account']} in project #{function['project']}", MU::ERR
|
625
|
+
ok = false
|
626
|
+
end
|
627
|
+
else
|
628
|
+
user = {
|
629
|
+
"name" => function['name'],
|
630
|
+
"cloud" => "Google",
|
631
|
+
"project" => function["project"],
|
632
|
+
"credentials" => function["credentials"],
|
633
|
+
"type" => "service"
|
634
|
+
}
|
635
|
+
if user["name"].length < 6
|
636
|
+
user["name"] += Password.pronounceable(6)
|
637
|
+
end
|
638
|
+
if function['roles']
|
639
|
+
user['roles'] = function['roles'].dup
|
640
|
+
end
|
641
|
+
configurator.insertKitten(user, "users", true)
|
642
|
+
function['dependencies'] ||= []
|
643
|
+
function['service_account'] = MU::Config::Ref.get(
|
644
|
+
type: "users",
|
645
|
+
cloud: "Google",
|
646
|
+
name: user["name"],
|
647
|
+
project: user["project"],
|
648
|
+
credentials: user["credentials"]
|
649
|
+
)
|
650
|
+
function['dependencies'] << {
|
651
|
+
"type" => "user",
|
652
|
+
"name" => user["name"]
|
653
|
+
}
|
654
|
+
end
|
655
|
+
|
656
|
+
# siblings = configurator.haveLitterMate?(nil, "vpcs", has_multiple: true)
|
657
|
+
# if siblings.size == 1
|
658
|
+
# MU.log "ContainerCluster #{function['name']} did not declare a VPC. Inserting into sibling VPC #{siblings[0]['name']}.", MU::WARN
|
659
|
+
# function["vpc"] = {
|
660
|
+
# "name" => siblings[0]['name'],
|
661
|
+
# "subnet_pref" => "all_private"
|
662
|
+
# }
|
663
|
+
# elsif MU::Cloud::Google.hosted? and MU::Cloud::Google.myVPCObj
|
664
|
+
# cluster["vpc"] = {
|
665
|
+
# "id" => MU.myVPC,
|
666
|
+
# "subnet_pref" => "all_private"
|
667
|
+
# }
|
668
|
+
# else
|
669
|
+
# MU.log "Cloud Function #{function['name']} must declare a VPC", MU::ERR
|
670
|
+
# ok = false
|
671
|
+
# end
|
672
|
+
|
673
|
+
ok
|
674
|
+
end
|
675
|
+
|
676
|
+
end
|
677
|
+
end
|
678
|
+
end
|
679
|
+
end
|
@@ -241,7 +241,7 @@ end
|
|
241
241
|
)
|
242
242
|
resp[args[:cloud_id]] = vpc if !vpc.nil?
|
243
243
|
rescue ::Google::Apis::ClientError => e
|
244
|
-
MU.log "
|
244
|
+
MU.log "VPC #{args[:cloud_id]} in project #{args[:project]} does not exist, or I do not have permission to view it", MU::WARN
|
245
245
|
end
|
246
246
|
else # XXX other criteria
|
247
247
|
vpcs = begin
|