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,257 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # NOTE
4
+ #
5
+ # table of equivalent commands for `gcloud container ...` commands..
6
+ #
7
+ # convert node-pool parameters to cluster create and cluster resize parameters..
8
+ #
9
+ # clusters create - clusters resize - nodepool create
10
+ #
11
+ # additional-zones - -
12
+ # async - async -
13
+ # cluster-ipv4-cidr - -
14
+ # disable-addons - -
15
+ # disk-size - - disk-size
16
+ # no-enable-cloud-endpoints - - enable-cloud-endpoints
17
+ # no-enable-cloud-logging - -
18
+ # no-enable-cloud-monitoring - -
19
+ # image-type - - image-type
20
+ # machine-type - - machine-type
21
+ # max-nodes-per-pool - -
22
+ # network - -
23
+ # num-nodes - size - num-nodes
24
+ # password - -
25
+ # scopes - - scopes
26
+ # subnetwork - -
27
+ # username - -
28
+ # wait - wait -
29
+ # zone - zone - zone
30
+ # - - - cluster
31
+ # - - node-pool - -
32
+ #
33
+
34
+ module GClouder
35
+ module Resources
36
+ module Container
37
+ module Clusters
38
+ include GClouder::Logging
39
+ include GClouder::Config::Project
40
+ include GClouder::Resource::Cleaner
41
+
42
+ module Config
43
+ def self.merged(config)
44
+ return unless config.key?("regions")
45
+ config["regions"].each do |region, region_config|
46
+ next unless region_config.key?("clusters")
47
+
48
+ region_config["clusters"].each_with_index do |cluster, cluster_index|
49
+ cluster_config = config["regions"][region]["clusters"][cluster_index]
50
+ config["regions"][region]["clusters"][cluster_index] = GClouder::Config::Cluster.new(cluster_config)
51
+
52
+ config["regions"][region]["clusters"][cluster_index]["node_pools"].each_with_index do |pool, pool_index|
53
+ pool_config = config["regions"][region]["clusters"][cluster_index]["node_pools"][pool_index]
54
+ config["regions"][region]["clusters"][cluster_index]["node_pools"][pool_index] = GClouder::Config::Cluster.new(pool_config)
55
+ end
56
+ end
57
+ end
58
+
59
+ config
60
+ end
61
+ end
62
+
63
+ def self.header(stage = :ensure)
64
+ info "[#{stage}] container / clusters", title: true
65
+ end
66
+
67
+ def self.validate
68
+ return if Local.list.empty?
69
+ header :validate
70
+ Local.validate
71
+ end
72
+
73
+ def self.ensure
74
+ return if Local.list.empty?
75
+ header
76
+
77
+ Local.list.each do |region, region_config|
78
+ info region, heading: true, indent: 2
79
+ region_config.each do |cluster|
80
+ info
81
+ Cluster.build(region, cluster)
82
+ end
83
+ end
84
+ end
85
+
86
+ module Local
87
+ def self.list
88
+ Resources::Region.instances(path: section)
89
+ end
90
+
91
+ def self.section
92
+ ["clusters"]
93
+ end
94
+
95
+ def self.validate
96
+ Resources::Validate::Region.instances(
97
+ list,
98
+ required_keys: GClouder::Config::Arguments.required(["container", "clusters"]),
99
+ permitted_keys: GClouder::Config::Arguments.permitted(["container", "clusters"]),
100
+ # FIXME: zone has wrong type in assets arguments file
101
+ # FIXME: num_nodes has wrong type in assets arguments file
102
+ ignore_keys: %w(node_pools zone num_nodes),
103
+ )
104
+ end
105
+ end
106
+
107
+ module Remote
108
+ def self.list
109
+ Resources::Remote.instances(
110
+ path: %w(container clusters),
111
+ # FIXME: zone has wrong type in assets arguments file
112
+ # FIXME: num_nodes has wrong type in assets arguments file
113
+ ignore_keys: %w(node_pools zone num_nodes),
114
+ )
115
+ end
116
+ end
117
+
118
+ module Cluster
119
+ include GClouder::Shell
120
+ include GClouder::Logging
121
+ include GClouder::Config::CLIArgs
122
+ include GClouder::Helpers
123
+ include GClouder::GCloud
124
+
125
+ def self.build(region, cluster)
126
+ unless cluster["zone"]
127
+ info "skipping cluster since no zone is set"
128
+ return
129
+ end
130
+
131
+ config = cluster.context(:create_cluster)
132
+
133
+ create(cluster["name"], config)
134
+
135
+ #check_immutable_conflicts(cluster)
136
+ end
137
+
138
+ #def self.check_immutable_conflicts(cluster)
139
+ # check_immutable_cluster_conflicts(cluster)
140
+ # check_immutable_nodepool_conflicts(cluster)
141
+ #end
142
+
143
+ #def self.check_immutable_cluster_conflicts(cluster)
144
+ # return unless Resource.resource?("container clusters", cluster["name"], silent: true)
145
+
146
+ # #info "checking cluster for immutable resource conflicts: #{cluster}"
147
+
148
+ # zone = cluster["zone"]
149
+
150
+ # remote_cluster_config = get_remote_cluster_config(cluster, zone)
151
+
152
+ # immutable_parameters = %w(cluster_ipv4_cidr current_node_count instance_group_urls locations
153
+ # node_pools services_ipv4_cidr username password name zone additional_zones network
154
+ # subnetwork node_config)
155
+
156
+ # cluster_config.each do |cluster_key, cluster_value|
157
+ # next if cluster_key == "node_pools"
158
+
159
+ # unless immutable_parameters.include?(cluster_key)
160
+ # #debug "skipping key since it isn't immutable: #{cluster_key}"
161
+ # next
162
+ # end
163
+
164
+ # if cluster_key == "additional_zones"
165
+ # remote_value = remote_cluster_config["locations"].sort
166
+ # cluster_value = [cluster_value + [cluster_config["zone"]]].flatten.sort
167
+ # else
168
+ # remote_value = remote_cluster_config[cluster_key.mixedcase]
169
+ # end
170
+
171
+ # check_values(cluster_key, remote_value, cluster_value)
172
+ # end
173
+ #end
174
+
175
+ #def self.check_immutable_nodepool_conflicts(cluster)
176
+ # intersection = nodepool_intersection(cluster)
177
+
178
+ # return if intersection.empty?
179
+
180
+ # #info "checking nodepools for immutable resource conflicts: #{intersection.join(", ")}"
181
+
182
+ # intersection.each do |pool|
183
+ # #debug "checking pool: #{pool}"
184
+
185
+ # zone = pool["zone"]
186
+
187
+ # pool.each do |pool_key, pool_value|
188
+ # immutable_parameters = %w(initial_node_count disk_size_gb service_account image_type machine_type scopes)
189
+
190
+ # # these keys are exposed through the pool resource but should be skipped..
191
+ # next if pool_key == "zone"
192
+ # next if pool_key == "additional_zones"
193
+
194
+ # unless immutable_parameters.include?(pool_key)
195
+ # #debug "skipping key since it isn't immutable: #{pool_key}"
196
+ # next
197
+ # end
198
+
199
+ # remote_nodepool_config = get_remote_nodepool_config(cluster["name"], pool["name"], zone)
200
+
201
+ # if pool_key == "scopes"
202
+ # check_values(pool_key, remote_nodepool_config["config"]["oauthScopes"], pool_value.sort)
203
+ # next
204
+ # end
205
+
206
+ # check_values(pool_key, remote_nodepool_config["config"][pool_key.mixedcase], pool_value)
207
+ # end
208
+ # end
209
+ #end
210
+
211
+ #def self.nodepool_intersection(cluster)
212
+ # zone = cluster["zone"]
213
+ # remote_nodepool_names(cluster["name"], zone) & local_nodepool_names(cluster)
214
+ #end
215
+
216
+ #def self.local_nodepool_names(cluster)
217
+ # cluster["node_pools"].map { |pool| pool["name"] }
218
+ #end
219
+
220
+ #def self.remote_nodepool_names(cluster_name, zone)
221
+ # gcloud("--format json container clusters describe #{cluster_name} --zone #{zone} | jq -r '.nodePools[].name'", force: true).split("\n")
222
+ #end
223
+
224
+ #def self.get_remote_nodepool_config(cluster_name, pool, zone)
225
+ # filter = "jq -r '.nodePools[] | select(.name == \"#{pool}\")'"
226
+ # gcloud("--format json container clusters describe #{cluster_name} --zone #{zone} | #{filter}", force: true)
227
+ #end
228
+
229
+ #def self.get_remote_cluster_config(cluster_name, zone)
230
+ # gcloud("--format json container clusters describe #{cluster_name} --zone #{zone}", force: true)
231
+ #end
232
+
233
+ #def self.check_values(key, remote_value, local_value)
234
+ # if remote_value != local_value
235
+ # fatal "error: remote config doesn't match local config: #{key} (#{remote_value} != #{local_value})"
236
+ # else
237
+ # #debug "local and remote keys have same value for param: #{key} = #{local_value}"
238
+ # true
239
+ # end
240
+ #end
241
+
242
+ def self.loop_until_cluster_exists(cluster_name)
243
+ until Resource.resource?("container clusters", cluster_name, silent: true)
244
+ sleep 1
245
+ end
246
+ end
247
+
248
+ def self.create(cluster_name, config)
249
+ args = hash_to_args(config)
250
+ Resource.ensure :"container clusters", cluster_name, args, indent: 3
251
+ loop_until_cluster_exists(cluster_name) if !cli_args[:dry_run]
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "json"
4
+
5
+ module GClouder
6
+ module Resources
7
+ module Container
8
+ module NodePools
9
+ include GClouder::Shell
10
+ include GClouder::Logging
11
+ include GClouder::GCloud
12
+ include GClouder::Helpers
13
+
14
+ def self.delete_default_nodepool
15
+ Resource.purge :"container node-pools", "default-pool"
16
+ end
17
+
18
+ def self.validate
19
+ return if GClouder::Resources::Container::Clusters::Local.list.empty?
20
+ header :validate
21
+
22
+ GClouder::Resources::Container::Clusters::Local.list.each do |region, clusters|
23
+ info region, heading: true, indent: 2
24
+ clusters.each do |cluster|
25
+ next if cluster["node_pools"].empty?
26
+ info cluster["name"], heading: true, indent: 3
27
+ Local.validate(cluster)
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.ensure
33
+ return if GClouder::Resources::Container::Clusters::Local.list.empty?
34
+ header
35
+
36
+ GClouder::Resources::Container::Clusters::Local.list.each do |region, clusters|
37
+ info region, heading: true, indent: 2
38
+ clusters.each do |cluster|
39
+ next if cluster["node_pools"].empty?
40
+ info cluster["name"], heading: true, indent: 3
41
+ cluster["node_pools"].each do |pool|
42
+ NodePool.create(cluster, pool)
43
+ NodePool.resize(cluster, pool) if Resource.resource?("container clusters", cluster["name"], silent: true)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.header(stage = :ensure)
50
+ info "[#{stage}] container / node-pools", title: true, indent: 1
51
+ end
52
+
53
+ # FIXME: create a collection then iterate through it to avoid printing
54
+ # messages when no clusters are undefined
55
+ def self.clean
56
+ return if GClouder::Resources::Container::Clusters::Local.list.empty?
57
+ header :clean
58
+
59
+ GClouder::Resources::Container::Clusters::Local.list.each do |region, clusters|
60
+ info region, heading: true, indent: 2
61
+ clusters.each do |cluster|
62
+ next if undefined(cluster).empty?
63
+
64
+ info cluster["name"], heading: true, indent: 3
65
+ undefined(cluster).each do |namespace, resources|
66
+ resources.each do |resource|
67
+ message = resource['name']
68
+ message += " (not defined locally)"
69
+ info
70
+ warning message, indent: 4
71
+ #resource_purge(namespace, user)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def self.undefined(cluster)
79
+ return {} unless Resource.resource?("container clusters", cluster["name"], silent: true)
80
+ self::Remote.list(cluster).each_with_object({}) do |(namespace, resources), collection|
81
+ resources.each do |resource|
82
+ namespace_resources = self::Local.list(cluster)[namespace]
83
+
84
+ next if namespace_resources && namespace_resources.select {|r| resource["name"] == r["name"] }.length > 0
85
+
86
+ collection[namespace] ||= []
87
+ collection[namespace] << resource
88
+ end
89
+ end
90
+ end
91
+
92
+ module Local
93
+ def self.list(cluster)
94
+ return {} unless cluster.key?("node_pools")
95
+ { cluster["zone"].gsub(/-.$/, "") => cluster["node_pools"] }
96
+ end
97
+
98
+ def self.section
99
+ %w(container node-pools)
100
+ end
101
+
102
+ def self.validate(cluster)
103
+ Resources::Validate::Region.instances(
104
+ list(cluster),
105
+ required_keys: GClouder::Config::Arguments.required(section).merge({ "zone" => { "type" => "String", "required" => "true" }}),
106
+ permitted_keys: GClouder::Config::Arguments.permitted(section).merge({ "additional_zones" => { "type" => "Array", "required" => "false" } }),
107
+ # FIXME: num_nodes has wrong type in assets arguments file..
108
+ ignore_keys: ["size", "num_nodes"],
109
+ skip_region: true,
110
+ indent: 1,
111
+ )
112
+ end
113
+ end
114
+
115
+ module Remote
116
+ def self.list(cluster)
117
+ Resources::Remote.instances(path: %w(container node-pools), args: "--cluster #{cluster['name']} --zone #{cluster['zone']}")
118
+ end
119
+ end
120
+
121
+ module NodePool
122
+ include GClouder::Shell
123
+ include GClouder::Logging
124
+ include GClouder::Config::CLIArgs
125
+ include GClouder::GCloud
126
+ include GClouder::Helpers
127
+
128
+ def self.create(cluster, pool)
129
+ parameters = hash_to_args(pool.context(:create_nodepool))
130
+ zone = pool["zone"]
131
+
132
+ if !check_exists?(cluster["name"], pool["name"], zone)
133
+ gcloud("alpha container node-pools create --cluster #{cluster["name"]} #{parameters} #{pool["name"]}")
134
+ if cli_args[:dry_run]
135
+ add pool["name"], indent: 4
136
+ else
137
+ sleep 1 until check_exists?(cluster["name"], pool["name"], zone)
138
+ end
139
+ else
140
+ good "#{pool["name"]}", indent: 4
141
+ end
142
+ end
143
+
144
+ def self.resize(cluster, pool)
145
+ config = pool.context(:resize_cluster)
146
+ zone = pool["zone"]
147
+
148
+ if !check_exists?(cluster["name"], pool["name"], zone)
149
+ info "skipping resize for non-existant cluster/nodepool: #{cluster["name"]}/#{pool["name"]}", indent: 5
150
+ return
151
+ end
152
+
153
+ number_of_zones = calculate_number_of_zones(pool)
154
+
155
+ parameters = hash_to_args(config)
156
+
157
+ current_size = remote_size(cluster["name"], pool["name"], pool["zone"])
158
+
159
+ desired_size = if pool.key?("additional_zones")
160
+ config["size"] * number_of_zones
161
+ else
162
+ config["size"]
163
+ end
164
+
165
+ if desired_size != current_size
166
+ add "resizing pool (zones: #{number_of_zones}): #{current_size} -> #{desired_size}", indent: 5
167
+ gcloud("container clusters resize #{cluster["name"]} --node-pool #{pool["name"]} #{parameters}")
168
+ else
169
+ true
170
+ end
171
+ end
172
+
173
+ def self.calculate_number_of_zones(pool)
174
+ pool.key?("additional_zones") ? (pool["additional_zones"].count + 1) : 1
175
+ end
176
+
177
+ def self.remote_size(cluster_name, pool_name, zone)
178
+ remote_pool_config = gcloud("--format json container clusters describe #{cluster_name} --zone #{zone} | jq --join-output -c '.nodePools[] | select(.name == \"#{pool_name}\")'", force: true)
179
+ instance_group_ids = remote_pool_config["instanceGroupUrls"].map { |url| data = url.split("/"); [ data[-1], data[-3] ] }.to_h
180
+ nodes = instance_group_ids.map do |instance_group_id, zone|
181
+ gcloud("--format json compute instance-groups describe --zone #{zone} #{instance_group_id} | jq '.size'", force: true).to_i
182
+ end
183
+ nodes.inject(0) { |sum, i| sum + i }
184
+ end
185
+
186
+ def self.check_exists?(cluster_name, nodepool, zone)
187
+ gcloud("--format json container node-pools list --zone #{zone} --cluster #{cluster_name} | jq 'select(.[].name == \"#{nodepool}\") | length'", force: true).to_i.nonzero?
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end