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,293 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Project
6
+ module IAMPolicyBinding
7
+ include GClouder::Config::Project
8
+ include GClouder::Logging
9
+ include GClouder::GCloud
10
+ include GClouder::Config::CLIArgs
11
+
12
+ def self.header(stage = :ensure)
13
+ info "[#{stage}] project / iam-policy-binding", indent: 1, title: true
14
+ end
15
+
16
+ def self.clean
17
+ return if undefined.empty?
18
+
19
+ header :clean
20
+
21
+ undefined.each do |region, roles|
22
+ info region, indent: 2, heading: true
23
+ roles.each do |role|
24
+ info role["name"], indent: 3, heading: true
25
+ role["members"].each do |member|
26
+ message = member
27
+ message += " (not defined locally)"
28
+ warning message, indent: 4
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def self.unmanaged_service_account?(member)
35
+ is_service_account?(member) && !member.include?(project["project_id"])
36
+ end
37
+
38
+ def self.is_service_account?(member)
39
+ member.include? "gserviceaccount.com"
40
+ end
41
+
42
+ def self.undefined
43
+ # each remote role
44
+ Remote.list.each_with_object({}) do |(region, remote_roles), collection|
45
+ # each role, {owner, ...}
46
+ remote_roles.each do |remote_role|
47
+ role_found = false
48
+
49
+ next unless remote_role.key?("members")
50
+
51
+ # for each remote member
52
+ remote_role["members"].each do |remote_member|
53
+
54
+ next unless Local.list.key?("global")
55
+
56
+ # see if the members role exists locally
57
+ Local.list["global"].each do |e|
58
+ next unless e["name"] == remote_role["name"]
59
+
60
+ role_found = true
61
+
62
+ # if it does then check if member is in member list for role
63
+
64
+ # member is defined, so skip it
65
+ next if e["members"].include?(remote_member)
66
+
67
+ # member is one we don't want to manage, so skip it
68
+ next if unmanaged_service_account?(remote_member)
69
+
70
+ # member is undefined so add it to collection
71
+
72
+ collection["global"] ||= []
73
+
74
+ # add role if it doesn't exist in collection
75
+ if !resource?(collection["global"], remote_role["name"])
76
+ collection["global"] << { "name" => remote_role["name"], "members" => [] }
77
+ end
78
+
79
+ # add memeber to role
80
+ resource_array_append(collection["global"], remote_role["name"], "members", remote_member)
81
+ end
82
+ end
83
+
84
+ # if entire role is missing from local..
85
+ next if role_found
86
+ collection["global"] ||= []
87
+ collection["global"] << remote_role
88
+ end
89
+ end
90
+ end
91
+
92
+ def self.resource?(resources, resource)
93
+ !resources.fetch_with_default("name", resource, {}).empty?
94
+ end
95
+
96
+ def self.resource_array_append(resources, resource_name, resource_key, obj)
97
+ resource = resources.fetch_with_default("name", resource_name, {})
98
+ resource[resource_key] ||= []
99
+ resource[resource_key] << obj
100
+ end
101
+
102
+ def self.validate
103
+ return if Local.list.empty?
104
+ header :validate
105
+
106
+ failure = false
107
+
108
+ Local.list.each do |region, roles|
109
+ info region, indent: 2, heading: true
110
+
111
+ next if roles.empty?
112
+
113
+ roles.each do |role|
114
+ if !role.is_a?(Hash)
115
+ bad "role type is not a Hash: #{role}"
116
+ failure = true
117
+ next
118
+ end
119
+
120
+ if !role.key?("name")
121
+ bad "missing key: name"
122
+ failure = true
123
+ next
124
+ end
125
+
126
+ if !role.key?("members")
127
+ bad "missing key: members"
128
+ failure = true
129
+ next
130
+ end
131
+
132
+ info role["name"], indent: 3, heading: true
133
+
134
+ unless role["members"].is_a?(Array)
135
+ bad "value not an array for key: #{role}", indent: 4
136
+ fatal "failure due to invalid config"
137
+ end
138
+
139
+ next unless role.key?("members")
140
+
141
+ role["members"].each do |member|
142
+
143
+ if !member.is_a?(String)
144
+ bad "member isn't a String: #{member}", indent: 3
145
+ failure = true
146
+ next
147
+ end
148
+
149
+ info member, indent: 4, heading: true
150
+
151
+ good "member is a String", indent: 5
152
+
153
+ case member
154
+ when /^user:/
155
+ good "member is a 'user'", indent: 5
156
+ when /^group:/
157
+ good "member is a 'group'", indent: 5
158
+ when /^serviceAccount:/
159
+ good "member is a 'serviceAccount'", indent: 5
160
+ when /^sink:/
161
+ good "member is a 'sink'", indent: 5
162
+ else
163
+ bad "member is an unknown type", indent: 5
164
+ failure = true
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ fatal "config validation failure" if failure
171
+ end
172
+
173
+ def self.sink(member)
174
+ gcloud("beta logging sinks describe #{member.gsub('sink:', '')} | jq -r .writer_identity", force: true).chomp
175
+ rescue
176
+ fatal "failed to lookup writer identity for sink: #{member}"
177
+ end
178
+
179
+ def self.ensure
180
+ return if Local.list.empty?
181
+ header
182
+
183
+ Local.list.each do |region, roles|
184
+ info region, indent: 2, heading: true
185
+
186
+ roles.each do |role|
187
+ info role["name"], indent: 3, heading: true
188
+
189
+ role["members"].each do |member|
190
+ if member.start_with?("sink:")
191
+ sink_name = member
192
+ member = sink(member)
193
+
194
+ if member.empty? && cli_args[:dry_run]
195
+ add "unknown - serviceAccount does not exist [#{sink_name}]", indent: 4
196
+ next
197
+ elsif member.empty?
198
+ fatal "unable to find sink serviceAccount (writer identity) - does sink exist for name: #{sink_name}"
199
+ end
200
+ end
201
+
202
+ if policy_member?(project_id, role["name"], member)
203
+ good member, indent: 4
204
+ next
205
+ end
206
+
207
+ if project_owner?
208
+ add member, indent: 4
209
+ Binding.ensure(project_id, member, role["name"])
210
+ next
211
+ end
212
+
213
+ add "#{member} [skipping] (insufficient permissions to create user)", indent: 4
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ def self.policy_member?(project, role, member)
220
+ bindings = gcloud("--format json projects get-iam-policy #{project} | jq '.bindings[] | select(.role == \"roles/#{role}\")'", force: true)
221
+ return false if bindings.empty?
222
+ fatal "could not get policy bindings for project: #{project}" unless bindings.key?("members")
223
+ bindings["members"].include?(member)
224
+ end
225
+
226
+ def self.project_id
227
+ project["project_id"]
228
+ end
229
+
230
+ def self.executioner
231
+ GClouder::Project::ID.id
232
+ end
233
+
234
+ def self.executioner_formatted
235
+ "user:#{executioner.strip}"
236
+ end
237
+
238
+ def self.project_owner?
239
+ return false unless executioner
240
+ policy_member?(project_id, "owner", executioner_formatted)
241
+ end
242
+
243
+ module Local
244
+ include GClouder::GCloud
245
+ include GClouder::Config::Project
246
+ include GClouder::Logging
247
+
248
+ def self.list
249
+ return {} unless project.key?("iam")
250
+ { "global" => project["iam"] }
251
+ end
252
+ end
253
+
254
+ module Remote
255
+ include GClouder::GCloud
256
+ include GClouder::Config::Project
257
+ include GClouder::Logging
258
+
259
+ def self.list
260
+ resources.each_with_object({ "global" => [] }) do |data, collection|
261
+ data["name"] = data["role"].gsub("roles/", "")
262
+ data.delete("role")
263
+ collection["global"] << data
264
+ end
265
+ end
266
+
267
+ def self.resources
268
+ gcloud("--format json projects get-iam-policy #{project_id} | jq .bindings", force: true)
269
+ end
270
+
271
+ def self.policy_member?(project, role, member)
272
+ bindings = gcloud("--format json projects get-iam-policy #{project} | jq '.bindings[] | select(.role == \"roles/#{role}\")'", force: true)
273
+ return false if bindings.empty?
274
+ fatal "could not get policy bindings for project: #{project}" unless bindings.key?("members")
275
+ bindings["members"].include?(member["name"])
276
+ end
277
+
278
+ def self.project_id
279
+ project["project_id"]
280
+ end
281
+ end
282
+
283
+ module Binding
284
+ include GClouder::GCloud
285
+
286
+ def self.ensure(project_id, name, role)
287
+ gcloud("projects add-iam-policy-binding #{project_id} --member='#{name}' --role='roles/#{role}'")
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Project
6
+ include GClouder::Logging
7
+ include GClouder::Config::Project
8
+ include GClouder::GCloud
9
+
10
+ def self.header(stage = :ensure)
11
+ info "[#{stage}] project", title: true
12
+ info
13
+ end
14
+
15
+ # unprivileged exists? method for use by non-billing accounts
16
+ def self.exists?
17
+ Resource.resource?("projects", project["project_id"], filter_key: "project_id", silent: true)
18
+ end
19
+
20
+ def self.update
21
+ header
22
+ Local.ensure
23
+ end
24
+
25
+ module Local
26
+ include GClouder::Config::Project
27
+ include GClouder::Logging
28
+ include GClouder::Shell
29
+ include GClouder::GCloud
30
+ include GClouder::Config::CLIArgs
31
+
32
+ def self.ensure
33
+ create_project
34
+ link_project_to_billing_account
35
+ end
36
+
37
+ def self.create_project
38
+ if exists?
39
+ good project_id, indent: 2
40
+ return
41
+ end
42
+
43
+ # FIXME: wait for project to exist and apis be enabled before continuing..
44
+ # FIXME: enable compute engine api..
45
+
46
+ add project_id, indent: 2
47
+ gcloud("alpha projects create #{project_id} --enable-cloud-apis --name=#{project_id}")
48
+
49
+ # FIXME: billing account isn't listed until linked..
50
+ #sleep 0.5 until exists? unless cli_args[:dry_run]
51
+ end
52
+
53
+ def self.link_project_to_billing_account
54
+ if linked_to_billing_account?
55
+ good "linked to billing account: #{account_id}", indent: 3
56
+ return
57
+ end
58
+
59
+ add "link to billing account: #{account_id}", indent: 3
60
+ gcloud("alpha billing accounts projects link #{project_id} --account-id=#{account_id}")
61
+ end
62
+
63
+ def self.linked_to_billing_account?
64
+ project_data(project_id)["billingEnabled"]
65
+ end
66
+
67
+ def self.exists?
68
+ ! project_data(project_id).empty?
69
+ end
70
+
71
+ def self.project_data(project)
72
+ shell("gcloud --format json alpha billing accounts projects list #{account_id} | jq '.[] | select(.projectId == \"#{project}\")'")
73
+ end
74
+
75
+ def self.project_id
76
+ project["project_id"]
77
+ end
78
+
79
+ def self.account_id
80
+ project["account_id"]
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Project
5
+ module ID
6
+ include GClouder::Config::Project
7
+ include GClouder::Config::CLIArgs
8
+ include GClouder::Shell
9
+
10
+ def self.id
11
+ @id
12
+ end
13
+
14
+ def self.load
15
+ @id ||= current
16
+ switch(project["project_id"]) if @id.nil?
17
+ end
18
+
19
+ def self.current
20
+ id = shell("gcloud auth list --format json | jq -r '.[] | select(.status == \"ACTIVE\") | .account'")
21
+ return id if !id.empty?
22
+ return if cli_args[:activate_service_accounts]
23
+ bail
24
+ end
25
+
26
+ def self.bail
27
+ puts "not authenticated against API and --activate-service-accounts option not passed"
28
+ puts ""
29
+ puts "please either:"
30
+ puts ""
31
+ puts " run: gcloud auth login && gcloud auth application-default login"
32
+ puts ""
33
+ puts " or: specify --activate-service-accounts flag and make sure the relevant keys exist in the keys dir"
34
+ puts ""
35
+ exit 1
36
+ end
37
+
38
+ def self.switch(project_id)
39
+ return unless project_id
40
+ if cli_args[:activate_service_accounts]
41
+ shell("gcloud --quiet auth activate-service-account --key-file #{key_file(project_id)}")
42
+ end
43
+ end
44
+
45
+ def self.rescue
46
+ if @id.nil?
47
+ shell("gcloud config unset account")
48
+ return
49
+ end
50
+
51
+ switch(@id)
52
+ end
53
+
54
+ def self.reset
55
+ switch(project["project_id"])
56
+ end
57
+
58
+ def self.default
59
+ shell("gcloud config set account #{project['project_id']}")
60
+ end
61
+
62
+ def self.dir
63
+ cli_args[:keys_dir] || File.join(ENV["HOME"], "keys")
64
+ end
65
+
66
+ def self.key_file(project_id)
67
+ File.join(dir, "gcloud-service-key-#{project_id}.json")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module PubSub
6
+ module Subscriptions
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}] pub/sub / subscriptions", 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, subscriptions|
21
+ info region, indent: 2, heading: true
22
+ info
23
+ subscriptions.each do |subscription|
24
+ Subscription.ensure(subscription)
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
+ # FIXME: improve validation
41
+ def self.validate
42
+ return if list.empty?
43
+
44
+ failure = false
45
+
46
+ list.each do |region, subscriptions|
47
+ info region, indent: 2, heading: true
48
+ subscriptions.each do |subscription|
49
+ info subscription["name"], indent: 3, heading: true
50
+ if !subscription["name"].is_a?(String)
51
+ bad "#{subscription['name']} is incorrect type #{subscription['name'].class}, should be: String", indent: 4
52
+ failure = true
53
+ end
54
+
55
+ if cli_args[:debug] || !cli_args[:output_validation]
56
+ good "name is a String", indent: 4
57
+ end
58
+ end
59
+ end
60
+
61
+ fatal "\nerror: validation failure" if failure
62
+ end
63
+
64
+ def self.list
65
+ GClouder::Resources::Global.instances(path: %w(pubsub subscriptions))
66
+ end
67
+ end
68
+
69
+ module Remote
70
+ def self.list
71
+ { "global" => instances.fetch("global", []).map { |subscription| { "name" => subscription["subscription_id"] } } }.delete_if { |_k, v| v.empty? }
72
+ end
73
+
74
+ def self.instances
75
+ Resources::Remote.instances(
76
+ path: %w(beta pubsub subscriptions)
77
+ )
78
+ end
79
+ end
80
+
81
+ module Subscription
82
+ include GClouder::GCloud
83
+ include GClouder::Helpers
84
+
85
+ def self.args(subscription)
86
+ hash_to_args(subscription)
87
+ end
88
+
89
+ def self.ensure(subscription)
90
+ Resource.ensure :"beta pubsub subscriptions", subscription["name"], args(subscription), filter_key: "subscriptionId", indent: 3
91
+ end
92
+
93
+ def self.purge(subscription)
94
+ Resource.purge :"beta pubsub subscriptions", subscription["name"]
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module PubSub
6
+ module Topics
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}] pub/sub / topics", 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, topics|
21
+ info region, indent: 2, heading: true
22
+ info
23
+ topics.each do |topic|
24
+ Topic.ensure(topic["name"])
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
+ # FIXME: improve validation
41
+ def self.validate
42
+ return if list.empty?
43
+
44
+ failure = false
45
+
46
+ list.each do |region, topics|
47
+ info region, indent: 2, heading: true
48
+ topics.each do |topic|
49
+ info topic["name"], indent: 3, heading: true
50
+ if !topic["name"].is_a?(String)
51
+ bad "#{topic['name']} is incorrect type #{topic['name'].class}, should be: String", indent: 4
52
+ failure = true
53
+ end
54
+
55
+ if cli_args[:debug] || !cli_args[:output_validation]
56
+ good "name is a String", indent: 4
57
+ end
58
+ end
59
+ end
60
+
61
+ fatal "\nerror: validation failure" if failure
62
+ end
63
+
64
+ def self.list
65
+ GClouder::Resources::Global.instances(path: %w(pubsub topics))
66
+ end
67
+ end
68
+
69
+ module Remote
70
+ def self.list
71
+ { "global" => instances.fetch("global", []).map { |topic| { "name" => topic["topic_id"] } } }.delete_if { |_k, v| v.empty? }
72
+ end
73
+
74
+ def self.instances
75
+ Resources::Remote.instances(
76
+ path: %w(beta pubsub topics)
77
+ )
78
+ end
79
+ end
80
+
81
+ module Topic
82
+ include GClouder::GCloud
83
+
84
+ def self.ensure(topic)
85
+ Resource.ensure :"beta pubsub topics", topic, filter_key: "topicId"
86
+ end
87
+
88
+ def self.purge(topic)
89
+ Resource.purge :"beta pubsub topics", topic
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end