cloud-mu 3.1.1 → 3.1.2beta2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|