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.
@@ -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