gclouder 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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