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,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