gclouder 0.1.1

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +104 -0
  3. data/bin/gclouder +7 -0
  4. data/lib/gclouder/config/arguments.rb +35 -0
  5. data/lib/gclouder/config/cli_args.rb +77 -0
  6. data/lib/gclouder/config/cluster.rb +66 -0
  7. data/lib/gclouder/config/defaults.rb +35 -0
  8. data/lib/gclouder/config/files/project.rb +24 -0
  9. data/lib/gclouder/config/project.rb +34 -0
  10. data/lib/gclouder/config/resource_representations.rb +31 -0
  11. data/lib/gclouder/config_loader.rb +25 -0
  12. data/lib/gclouder/config_section.rb +26 -0
  13. data/lib/gclouder/dependencies.rb +11 -0
  14. data/lib/gclouder/gcloud.rb +39 -0
  15. data/lib/gclouder/gsutil.rb +25 -0
  16. data/lib/gclouder/header.rb +9 -0
  17. data/lib/gclouder/helpers.rb +77 -0
  18. data/lib/gclouder/logging.rb +181 -0
  19. data/lib/gclouder/mappings/argument.rb +31 -0
  20. data/lib/gclouder/mappings/file.rb +31 -0
  21. data/lib/gclouder/mappings/property.rb +31 -0
  22. data/lib/gclouder/mappings/resource_representation.rb +31 -0
  23. data/lib/gclouder/monkey_patches/array.rb +19 -0
  24. data/lib/gclouder/monkey_patches/boolean.rb +12 -0
  25. data/lib/gclouder/monkey_patches/hash.rb +44 -0
  26. data/lib/gclouder/monkey_patches/ipaddr.rb +10 -0
  27. data/lib/gclouder/monkey_patches/string.rb +30 -0
  28. data/lib/gclouder/resource.rb +63 -0
  29. data/lib/gclouder/resource_cleaner.rb +58 -0
  30. data/lib/gclouder/resources/compute/addresses.rb +108 -0
  31. data/lib/gclouder/resources/compute/bgp-vpns.rb +220 -0
  32. data/lib/gclouder/resources/compute/disks.rb +99 -0
  33. data/lib/gclouder/resources/compute/firewall_rules.rb +82 -0
  34. data/lib/gclouder/resources/compute/instances.rb +147 -0
  35. data/lib/gclouder/resources/compute/networks/subnets.rb +104 -0
  36. data/lib/gclouder/resources/compute/networks.rb +110 -0
  37. data/lib/gclouder/resources/compute/project_info/ssh_keys.rb +171 -0
  38. data/lib/gclouder/resources/compute/routers.rb +83 -0
  39. data/lib/gclouder/resources/compute/vpns.rb +199 -0
  40. data/lib/gclouder/resources/container/clusters.rb +257 -0
  41. data/lib/gclouder/resources/container/node_pools.rb +193 -0
  42. data/lib/gclouder/resources/dns.rb +390 -0
  43. data/lib/gclouder/resources/logging/sinks.rb +98 -0
  44. data/lib/gclouder/resources/project/iam_policy_binding.rb +293 -0
  45. data/lib/gclouder/resources/project.rb +85 -0
  46. data/lib/gclouder/resources/project_id.rb +71 -0
  47. data/lib/gclouder/resources/pubsub/subscriptions.rb +100 -0
  48. data/lib/gclouder/resources/pubsub/topics.rb +95 -0
  49. data/lib/gclouder/resources/storagebuckets.rb +103 -0
  50. data/lib/gclouder/resources/validate/global.rb +27 -0
  51. data/lib/gclouder/resources/validate/local.rb +68 -0
  52. data/lib/gclouder/resources/validate/region.rb +28 -0
  53. data/lib/gclouder/resources/validate/remote.rb +78 -0
  54. data/lib/gclouder/resources.rb +148 -0
  55. data/lib/gclouder/shell.rb +71 -0
  56. data/lib/gclouder/version.rb +5 -0
  57. data/lib/gclouder.rb +278 -0
  58. metadata +102 -0
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module DNS
6
+ include GClouder::GCloud
7
+ include GClouder::Config::Project
8
+ include GClouder::Config::CLIArgs
9
+ include GClouder::Logging
10
+
11
+ def self.clean
12
+ return if undefined.empty?
13
+ header :clean
14
+
15
+ undefined.each do |region, zones|
16
+ info region, indent: 2, heading: true
17
+ zones.each do |zone|
18
+ next unless zone.key?("records")
19
+ info zone["name"], indent: 3, heading: true
20
+ zone["records"].each do |record|
21
+ warning "#{record['name']} IN A #{record['type']} #{record['ttl']} #{record['rrdatas'].join(' ')}", indent: 4
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.update_zone_record(zones, zone, record, key, value)
28
+ record = zones.fetch_with_default("name", zone, {}).fetch("records", []).fetch_with_default("name", record, {})
29
+ fatal "couldn't update zone record" if record.empty?
30
+ record[key] = value
31
+ end
32
+
33
+ def self.record?(zones, zone, record)
34
+ zones.fetch_with_default("name", zone, {}).fetch("records", []).fetch_with_default("name", record, {}).empty?
35
+ end
36
+
37
+ def self.zone?(zones, zone)
38
+ zones.fetch_with_default("name", zone, {}).empty?
39
+ end
40
+
41
+ def self.zone_record?(zones, zone_name, record_name)
42
+ found_zone = zones.find { |z| z["name"] == zone_name }
43
+ return unless found_zone
44
+ found["records"].find { |r| r["name"] == record_name }.nil?
45
+ end
46
+
47
+ def self.zone_records_append(zones, zone, record)
48
+ zone = zones.fetch_with_default("name", zone, {})
49
+ fatal "couldn't update zone" if zone.empty?
50
+ zone["records"] << record
51
+ end
52
+
53
+ def self.undefined
54
+ return {} if Remote.list.empty?
55
+
56
+ Remote.list.each_with_object({ "global" => [] }) do |(_region, zones), collection|
57
+ zones.each do |zone|
58
+ # if zone isnt defined locally, then add it along with its associated records
59
+ if !zone?(zones, zone["name"])
60
+ collection["global"] << zone
61
+ next
62
+ end
63
+
64
+ next unless zone.key?("records")
65
+
66
+ # if record isnt defined locally, create a zone in global (if one doesn't exist), then append record to records field
67
+ zone["records"].each do |record|
68
+ if !zone?(collection["global"], zone["name"])
69
+ zone_collection = zone.dup
70
+ zone_collection["records"] = []
71
+ collection["global"] << zone_collection
72
+ end
73
+
74
+ if !record?(zones, zone["name"], record["name"])
75
+ zone_records_append(collection["global"], zone["name"], record)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def self.validate
83
+ return if Local.list.empty?
84
+ header :validate
85
+
86
+ failure = false
87
+
88
+ Local.list.each do |region, zones|
89
+ info region, indent: 2, heading: true
90
+
91
+ unless zones.is_a?(Array)
92
+ failure = true
93
+ bad "zones value should be an array", indent: 3, heading: true
94
+ next
95
+ end
96
+
97
+ zones.each do |zone|
98
+ unless zone.is_a?(Hash)
99
+ failure = true
100
+ bad "zone value should be a hash", indent: 3, heading: true
101
+ next
102
+ end
103
+
104
+ unless zone.key?("name")
105
+ failure = true
106
+ bad "zone with missing key: name", indent: 3, heading: true
107
+ next
108
+ end
109
+
110
+ if zone["name"] !~ /^[a-z0-9\-]+$/
111
+ failure = true
112
+ bad "zone name must only contain lower-case letters, digits or dashes"
113
+ next
114
+ end
115
+
116
+ info zone["name"], indent: 3, heading: true
117
+
118
+ if zone.key?("zone")
119
+ good "resource has zone specified (#{zone['zone']})", indent: 4
120
+ else
121
+ failure = true
122
+ bad "missing key: zone", indent: 4
123
+ end
124
+
125
+ next unless zone.key?("records")
126
+
127
+ zone["records"].each do |record|
128
+ info record["name"], indent: 4, heading: true
129
+ if ["A", "CNAME", "PTR", "NS", "TXT"].include?(record["type"])
130
+ good "record has valid type (#{record['type']})", indent: 5
131
+ else
132
+ bad "unknown record type: #{record['type']}", indent: 5
133
+ failure = true
134
+ end
135
+
136
+ if record["ttl"].is_a?(Integer)
137
+ good "record has valid ttl (#{record['ttl']})", indent: 5
138
+ else
139
+ bad "record has invalid ttl: #{record['ttl']}", indent: 5
140
+ failure = true
141
+ end
142
+
143
+ if record.key?("value") || record.key?("static_ips")
144
+ good "record has a target", indent: 5
145
+ else
146
+ bad "record has no target", indent: 5
147
+ failure = true
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ fatal "failure due to invalid config" if failure
154
+ end
155
+
156
+ def self.header(stage = :ensure)
157
+ info "[#{stage}] dns", indent: 1, title: true
158
+ end
159
+
160
+ def self.ensure
161
+ return if Local.list.empty?
162
+
163
+ header
164
+
165
+ Local.list.each do |region, zones|
166
+ info region, heading: true, indent: 2
167
+
168
+ zones.each do |zone|
169
+ project_id = zone_project_id(zone)
170
+
171
+ next if skip?(project_id, zone)
172
+
173
+ info
174
+ Zone.ensure(project_id, zone["name"], zone["zone"])
175
+ Records.ensure(project_id, zone)
176
+ end
177
+ end
178
+ end
179
+
180
+ def self.skip?(project_id, zone)
181
+ return false if project_id == project["project_id"]
182
+ return false if !cli_args[:skip_cross_project_resources]
183
+
184
+ extra_info = " [#{project_id}]" if project_id != project["project_id"]
185
+ warning "#{zone['name']}#{extra_info} [skipping] (cross project resource)", indent: 3, heading: true
186
+ true
187
+ end
188
+
189
+ def self.zone_project_id(zone_config)
190
+ return project["project_id"] unless zone_config
191
+ zone_config.key?("project_id") ? zone_config["project_id"] : project["project_id"]
192
+ end
193
+
194
+ module Zone
195
+ include GClouder::Config::Project
196
+
197
+ def self.ensure(project_id, name, zone)
198
+ extra_info = (project_id != project["project_id"]) ? "[#{project_id}]" : ""
199
+
200
+ Resource.ensure :"dns managed-zones", name,
201
+ "--dns-name=#{zone} --description='Created by GClouder'", project_id: project_id, extra_info: extra_info
202
+ end
203
+ end
204
+
205
+ module Records
206
+ include GClouder::Logging
207
+ include GClouder::Config::CLIArgs
208
+ include GClouder::GCloud
209
+
210
+ def self.ensure(project_id, zone)
211
+ return unless zone.key?("records")
212
+
213
+ start_transaction(project_id, zone["name"])
214
+
215
+ zone["records"].each do |record|
216
+ next unless record_is_valid(record)
217
+
218
+ values = []
219
+
220
+ if record.key?("value") && record["value"].is_a?(Array)
221
+ values << record["value"].join(" ")
222
+
223
+ elsif record.key?("value") && record["value"].is_a?(String)
224
+ values << record["value"]
225
+
226
+ elsif record.key?("static_ips")
227
+ record["static_ips"].each do |ip|
228
+ values << static_ip(project_id, zone["name"], ip)
229
+ end
230
+
231
+ else
232
+ bad "no 'value' or 'static_ips' key found for record: #{record["name"]}"
233
+ fatal "failure due to invalid config"
234
+ end
235
+
236
+ values.each do |value|
237
+ unless record["name"].match(/\.$/)
238
+ bad "record name missing '.' suffix: #{record["name"]}"
239
+ fatal "failure due to invalid config"
240
+ end
241
+ ttl = record.key?("ttl") ? record["ttl"] : "300"
242
+ add_record_set record["name"], value, zone["name"], record["type"], ttl, project_id
243
+ end
244
+ end
245
+
246
+ execute_transaction(project_id, zone["name"])
247
+ end
248
+
249
+ def self.start_transaction(project_id, zone_name)
250
+ gcloud "dns record-sets transaction start --zone=#{zone_name}", project_id: project_id
251
+ end
252
+
253
+ def self.execute_transaction(project_id, zone_name)
254
+ gcloud "dns record-sets transaction execute --zone=#{zone_name}", project_id: project_id
255
+ end
256
+
257
+ def self.abort_transaction(args, project_id)
258
+ info "aborting dns record-set transaction", indent: 4
259
+ gcloud "dns record-sets transaction abort #{args}", project_id: project_id
260
+ # FIXME: remove transaction file..
261
+ end
262
+
263
+ def self.record_is_valid(record)
264
+ if record["type"] == "CNAME" && !record["value"].end_with?(".")
265
+ info "CNAME value must end with '.'"
266
+ return false
267
+ end
268
+
269
+ true
270
+ end
271
+
272
+ def self.add_record_set(name, value, zone, type, ttl, project_id)
273
+ if Resource.resource?("dns record-sets", name, "--zone=#{zone}", filter: "name = #{name} AND type = #{type}", project_id: project_id, silent: true)
274
+ good "#{name} IN #{type} #{value} #{ttl}", indent: 4
275
+ return
276
+ end
277
+
278
+ add "#{name} IN #{type} #{value} #{ttl}", indent: 4
279
+
280
+ gcloud "dns record-sets transaction add --name=#{name} --zone=#{zone} --type=#{type} --ttl=#{ttl} #{value}", project_id: project_id
281
+ end
282
+
283
+ def self.lookup_ip(name, context)
284
+ args = context == "global" ? "--global" : "--regions #{context}"
285
+ ip = gcloud("compute addresses list #{name} #{args}", force: true)
286
+ return false if ip.empty?
287
+ ip[0]["address"]
288
+ end
289
+
290
+ def self.static_ip(project_id, zone_name, static_ip_config)
291
+ %w(name context).each do |key|
292
+ unless static_ip_config[key]
293
+ bad "missing key '#{key}' for record"
294
+ abort_transaction "--zone=#{zone_name}", project_id
295
+ fatal "failure due to invalid config"
296
+ end
297
+ end
298
+
299
+ name = static_ip_config["name"]
300
+ context = static_ip_config["context"]
301
+
302
+ ip = lookup_ip(name, context)
303
+
304
+ unless ip
305
+ unless cli_args[:dry_run]
306
+ bad "ip address not found for context/name: #{context}/#{name}"
307
+ abort_transaction "--zone=#{zone_name}", project_id
308
+ fatal "failure due to invalid config"
309
+ end
310
+
311
+ # on dry runs assume the ip address has not been created but config is valid
312
+ ip = "<#{context}/#{name}>"
313
+ end
314
+
315
+ ip
316
+ end
317
+
318
+ def self.describe_zone(project_id, zone_name)
319
+ gcloud "--format json dns managed-zones describe #{zone_name}", project_id: project_id, force: true
320
+ end
321
+
322
+ def self.zone_nameservers(project_id, zone_name)
323
+ remote_zone_definition = describe_zone(project_id, zone_name)
324
+ fatal "nameservers not found for zone: #{zone_name}" unless remote_zone_definition.key?("nameServers")
325
+ remote_zone_definition["nameServers"]
326
+ end
327
+
328
+ def self.dependencies
329
+ return unless project.key?("dns")
330
+ return unless project["dns"].key?("zones")
331
+
332
+ project["dns"]["zones"].each do |zone, zone_config|
333
+ project_id = zone_project_id(zone_config)
334
+ zone_name = zone.tr(".", "-")
335
+
336
+ # skip zone unless manage_nameservers is true
337
+ next unless zone_config.key?("manage_nameservers")
338
+ next unless zone_config["manage_nameservers"]
339
+
340
+ # parent zone data
341
+ parent_zone = zone.split(".")[1..-1].join(".")
342
+ parent_zone_name = parent_zone.tr(".", "-")
343
+
344
+ parent_zone_config = project["dns"]["zones"][parent_zone]
345
+
346
+ # get project_id for parent zone - if it isn't set then assume the zone exists in current project
347
+ parent_project_id = parent_zone_config.key?("project_id") ? parent_zone_config["project_id"] : project_id
348
+
349
+ info "ensuring nameservers for zone: #{zone}, project_id: #{parent_project_id}, parent_zone: #{parent_zone}"
350
+
351
+ next if cli_args[:dry_run]
352
+
353
+ # find nameservers for this zone
354
+ nameservers = zone_nameservers(project_id, zone_name)
355
+
356
+ # ensure parent zone exists
357
+ create_zone(parent_project_id, parent_zone, parent_zone_name)
358
+
359
+ # create nameservers in parent zone
360
+ start_transaction(parent_project_id, parent_zone_name)
361
+ add_record_set zone, nameservers.join(" "), parent_zone_name, "NS", 600, parent_project_id
362
+ execute_transaction(parent_project_id, parent_zone_name)
363
+ end
364
+ end
365
+ end
366
+
367
+ module Local
368
+ def self.list
369
+ GClouder::Resources::Global.instances(path: %w(dns zones))
370
+ end
371
+ end
372
+
373
+ module Remote
374
+ def self.list
375
+ zones.each_with_object({ "global" => [] }) do |zone, collection|
376
+ collection["global"] << { "name" => zone["name"], "records" => records(zone["name"]) }
377
+ end.delete_if { |_k, v| v.empty? }
378
+ end
379
+
380
+ def self.records(zone_name)
381
+ Resource.list("dns record-sets", "--zone #{zone_name}")
382
+ end
383
+
384
+ def self.zones
385
+ Resource.list("dns managed-zones").map { |zone| zone }
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Logging
6
+ module Sinks
7
+ include GClouder::Config::CLIArgs
8
+ include GClouder::Config::Project
9
+ include GClouder::Logging
10
+ include GClouder::Resource::Cleaner
11
+
12
+ def self.header(stage = :ensure)
13
+ info "[#{stage}] logging / sinks", indent: 1, title: true
14
+ end
15
+
16
+ def self.ensure
17
+ return if Local.list.empty?
18
+ header
19
+
20
+ Local.list.each do |region, sinks|
21
+ info region, indent: 2, heading: true
22
+ info
23
+ sinks.each do |sink|
24
+ Sink.ensure(sink)
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.validate
30
+ return if Local.list.empty?
31
+ header :validate
32
+ Local.validate
33
+ end
34
+
35
+ module Local
36
+ include GClouder::Config::CLIArgs
37
+ include GClouder::Config::Project
38
+ include GClouder::Logging
39
+
40
+ def self.validate
41
+ return if list.empty?
42
+
43
+ failure = false
44
+
45
+ list.each do |region, sinks|
46
+ info region, indent: 2, heading: true
47
+ sinks.each do |sink|
48
+ info sink["name"], indent: 3, heading: true
49
+ if !sink["name"].is_a?(String)
50
+ bad "#{sink['name']} is incorrect type #{sink['name'].class}, should be: String", indent: 4
51
+ failure = true
52
+ end
53
+
54
+ if cli_args[:debug] || !cli_args[:output_validation]
55
+ good "name is a String", indent: 4
56
+ end
57
+ end
58
+ end
59
+
60
+ fatal "\nerror: validation failure" if failure
61
+ end
62
+
63
+ def self.list
64
+ GClouder::Resources::Global.instances(path: %w(logging sinks))
65
+ end
66
+ end
67
+
68
+ module Remote
69
+ def self.list
70
+ { "global" => instances.fetch("global", []).map { |sink| { "name" => sink["sink_id"] } } }.delete_if { |_k, v| v.empty? }
71
+ end
72
+
73
+ def self.instances
74
+ Resources::Remote.instances(
75
+ path: %w(beta logging sinks)
76
+ )
77
+ end
78
+ end
79
+
80
+ module Sink
81
+ include GClouder::GCloud
82
+
83
+ def self.args(sink)
84
+ "#{sink['destination']} " + hash_to_args(sink.delete_if { |k| k == "destination" })
85
+ end
86
+
87
+ def self.ensure(sink)
88
+ Resource.ensure :"beta logging sinks", sink["name"], args(sink)
89
+ end
90
+
91
+ def self.purge(sink)
92
+ Resource.purge :"beta logging sinks", sink["name"]
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end