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.
@@ -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 "Do not have permissions to retrieve VPC #{args[:cloud_id]} in project #{args[:project]}", MU::WARN, details: caller
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