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