hub-clusters-creator 0.0.3

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.
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (C) 2019 Rohith Jayawardene <gambol99@gmail.com>
4
+ #
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License
7
+ # as published by the Free Software Foundation; either version 2
8
+ # of the License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+ require 'google/apis/compute_v1'
19
+ require 'google/apis/container_v1beta1'
20
+ require 'google/apis/dns_v1'
21
+ require 'googleauth'
22
+
23
+ require 'hub-clusters-creator/errors'
24
+ require 'hub-clusters-creator/kube/kube'
25
+ require 'hub-clusters-creator/logging'
26
+ require 'hub-clusters-creator/providers/bootstrap'
27
+ require 'hub-clusters-creator/providers/gke/helpers'
28
+
29
+ # rubocop:disable Metrics/ClassLength,Metrics/LineLength,Metrics/MethodLength
30
+ module HubClustersCreator
31
+ module Providers
32
+ # GKE provides the GKE implmentation
33
+ class GKE
34
+ DEFAULT_PSP_CLUSTER_ROLE = <<~YAML
35
+ apiVersion: rbac.authorization.k8s.io/v1
36
+ kind: ClusterRole
37
+ metadata:
38
+ name: default:psp
39
+ rules:
40
+ - apiGroups:
41
+ - policy
42
+ resourceNames:
43
+ - gce.unprivileged-addon
44
+ resources:
45
+ - podsecuritypolicies
46
+ verbs:
47
+ - use
48
+ YAML
49
+
50
+ DEFAULT_PSP_CLUSTERROLE_BINDING = <<~YAML
51
+ apiVersion: rbac.authorization.k8s.io/v1
52
+ kind: ClusterRoleBinding
53
+ metadata:
54
+ name: default:psp
55
+ roleRef:
56
+ apiGroup: rbac.authorization.k8s.io
57
+ kind: ClusterRole
58
+ name: default:psp
59
+ subjects:
60
+ - apiGroup: rbac.authorization.k8s.io
61
+ kind: Group
62
+ name: system:authenticated
63
+ - apiGroup: rbac.authorization.k8s.io
64
+ kind: Group
65
+ name: system:serviceaccounts
66
+ YAML
67
+
68
+ # Compute are a collection of methods used to interact with GCP
69
+ include Errors
70
+ include GCP::Compute
71
+ include GCP::Containers
72
+ include Logging
73
+
74
+ Container = Google::Apis::ContainerV1beta1
75
+ Compute = Google::Apis::ComputeV1
76
+ Dns = Google::Apis::DnsV1
77
+
78
+ def initialize(provider)
79
+ @account = provider[:account]
80
+ @project = provider[:project]
81
+ @region = provider[:region]
82
+ @compute = Compute::ComputeService.new
83
+ @gke = Container::ContainerService.new
84
+ @dns = Dns::DnsService.new
85
+ @client = nil
86
+
87
+ @compute.authorization = authorize
88
+ @gke.authorization = authorize
89
+ @dns.authorization = authorize
90
+ end
91
+
92
+ # create is responsible for building the infrastructure
93
+ def create(name, config)
94
+ # @step: validate the configuration
95
+ begin
96
+ validate(config)
97
+ rescue StandardError => e
98
+ raise ConfigurationError, "invalid configuration, error: #{e}"
99
+ end
100
+
101
+ # @step: provision the infrastructure
102
+ begin
103
+ provision_gke(name, config)
104
+ rescue StandardError => e
105
+ raise InfrastructureError, "failed to provision cluster: '#{name}', error: #{e}"
106
+ end
107
+
108
+ # @step: initialize the cluster
109
+ begin
110
+ c = provision_cluster(name, config)
111
+ rescue StandardError => e
112
+ raise InitializerError, "failed to initialize the cluster: '#{name}', error: #{e}"
113
+ end
114
+
115
+ {
116
+ cluster: {
117
+ ca: c.master_auth.cluster_ca_certificate,
118
+ endpoint: "https://#{c.endpoint}",
119
+ token: @client.account('sysadmin')
120
+ },
121
+ config: config,
122
+ services: {
123
+ grafana: {
124
+ hostname: config[:grafana_hostname]
125
+ }
126
+ }
127
+ }
128
+ end
129
+
130
+ # destroy is used to kill off a cluster
131
+ def destroy(name)
132
+ @gke.delete_project_location_cluster("projects/#{@project}/locations/#{@region}/clusters/#{name}")
133
+ end
134
+
135
+ private
136
+
137
+ # provision_gke is responsible for provisioning the infrastucture
138
+ # rubocop:disable Metrics/AbcSize
139
+ def provision_gke(name, config)
140
+ info "checking if the gke cluster: '#{name}' exists"
141
+ if cluster?(name)
142
+ info "skipping the creation of cluster: '#{name}' as it already exists"
143
+ else
144
+ info "cluster: '#{name}' does not exist, creating now"
145
+ path = "projects/#{@project}/locations/#{@region}"
146
+ operation = @gke.create_project_location_cluster(path, cluster_spec(config))
147
+
148
+ info "waiting for the cluster: '#{name}' to be created, operation: '#{operation.name}'"
149
+ status = hold_for_operation(operation.name)
150
+ unless status.status_message.nil?
151
+ raise InfrastructureError, "operation: '#{x.operation_type}' failed, error: #{x.status_message}"
152
+ end
153
+ end
154
+ gke = cluster(name)
155
+
156
+ # @step: create a cloud-nat device if private networking enabled
157
+ # and nothing exists already
158
+ if config[:enable_private_network]
159
+ info 'checking if cloud-nat device has been created'
160
+ router('router') do |x|
161
+ unless x.nats
162
+ x.nats = default_cloud_nat('cloud-nat')
163
+ patch_router('router', x)
164
+ end
165
+ end
166
+ end
167
+
168
+ info "provisioning a dns entry for the master api = > #{gke.endpoint}"
169
+ # dns(kubeapi_name(config).to_s, gke.endpoint, config[:domain])
170
+ end
171
+ # rubocop:enable Metrics/AbcSize
172
+
173
+ # provision_cluster is responsible for kickstarting the cluster
174
+ # rubocop:disable Metrics/AbcSize
175
+ def provision_cluster(name, config)
176
+ info "waiting for the master api endpoint to be available on cluster: #{name}"
177
+ thing = cluster(name)
178
+ @client = HubClustersCreator::Kube.new(thing.endpoint, token: authorize.access_token)
179
+ @client.wait_for_kubeapi
180
+
181
+ # @step: if psp is enabled we need to add the roles and bindings
182
+ info 'creating the default psp binding to unpriviledged policy'
183
+ @client.kubectl(DEFAULT_PSP_CLUSTER_ROLE)
184
+ @client.kubectl(DEFAULT_PSP_CLUSTERROLE_BINDING)
185
+
186
+ # @step: bootstrap the cluster and wait
187
+ HubClustersCreator::Providers::Bootstrap.new(name, @client, config).bootstrap
188
+
189
+ ingress = @client.get('loki-grafana', 'loki', 'ingresses', version: 'extensions/v1beta1')
190
+ address = ingress.status.loadBalancer.ingress.first.ip
191
+
192
+ # @step: update the dns record for the ingress
193
+ unless (config[:grafana_hostname] || '').empty?
194
+ info "adding a dns record for #{config[:grafana_hostname]} => #{address}"
195
+ dns(config[:grafana_hostname].split('.').first, address, config[:domain])
196
+ end
197
+
198
+ cluster(name)
199
+ end
200
+ # rubocop:enable Metrics/AbcSize
201
+
202
+ # validate is responsible for validating the options for cluster creation
203
+ # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
204
+ def validate(config)
205
+ raise ConfigurationError, "domain: #{config[:domain]} does not exist within project" unless domain?(config[:domain])
206
+ raise ConfigurationError, 'disk size must be positive' unless config[:disk_size_gb].positive?
207
+ raise ConfigurationError, 'size must be positive' unless config[:size].positive?
208
+
209
+ # @check the networking options
210
+ raise ConfigurationError, 'the network does not exist' unless network?(config[:network])
211
+ raise ConfigurationError, 'the subnetwork does not exist' unless subnet?(config[:subnetwork], config[:network]) && !config[:create_subnetwork]
212
+
213
+ # @check if subnets exist - need to do something more clever
214
+ # and check for overlapping subnety really but i can't find a gem
215
+ network_checks = []
216
+ network_checks.push(config['cluster_ipv4_cidr']) if config['cluster_ipv4_cidr']
217
+ network_checks.push(config['master_ipv4_cidr_block']) if config['master_ipv4_cidr_block']
218
+ network_checks.push(config['services_ipv4_cidr']) if config['services_ipv4_cidr']
219
+
220
+ nets = networks
221
+ network_checks.each do |n|
222
+ nets.each { |x| raise ConfigurationError, "network: #{n} already exists" if n == x.cidr }
223
+ end
224
+
225
+ if config[:enable_private_network] && !config[:master_ipv4_cidr_block]
226
+ raise ConfigurationError, 'you must specify a master_ipv4_cidr_block'
227
+ end
228
+
229
+ config
230
+ end
231
+ # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
232
+
233
+ # patch_router is a wrapper for the patch router
234
+ def patch_router(name, router)
235
+ @compute.patch_router(@project, @region, name, router)
236
+ end
237
+
238
+ # default_cloud_nat returns a default cloud nat configuration
239
+ def default_cloud_nat(name = 'cloud-nat')
240
+ [
241
+ Google::Apis::ComputeV1::RouterNat.new(
242
+ log_config: Google::Apis::ComputeV1::RouterNatLogConfig.new(enable: false, filter: 'ALL'),
243
+ name: name,
244
+ nat_ip_allocate_option: 'AUTO_ONLY',
245
+ source_subnetwork_ip_ranges_to_nat: 'ALL_SUBNETWORKS_ALL_IP_RANGES'
246
+ )
247
+ ]
248
+ end
249
+
250
+ # authorize is responsible for providing an access token to operate
251
+ def authorize(scopes = ['https://www.googleapis.com/auth/cloud-platform'])
252
+ if @authorizer.nil?
253
+ @authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
254
+ json_key_io: StringIO.new(@account),
255
+ scope: scopes
256
+ )
257
+ @authorizer.fetch_access_token!
258
+ end
259
+ @authorizer
260
+ end
261
+ end
262
+ end
263
+ end
264
+ # rubocop:enable Metrics/ClassLength,Metrics/LineLength,Metrics/MethodLength
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (C) 2019 Rohith Jayawardene <gambol99@gmail.com>
4
+ #
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License
7
+ # as published by the Free Software Foundation; either version 2
8
+ # of the License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ # rubocop:disable Metrics/LineLength,Metrics/MethodLength,Metrics/ModuleLength
20
+ module HubClustersCreator
21
+ module Providers
22
+ # GCP is the namespace
23
+ module GCP
24
+ # Containers is a GKE container methods
25
+ module Containers
26
+ private
27
+
28
+ # gke_locations returns a list of compute locations
29
+ def gke_locations
30
+ @gke.list_project_locations("projects/#{@project}").locations.select do |x|
31
+ x.name.start_with?("#{@region}-")
32
+ end.map(&:name)
33
+ end
34
+
35
+ # operation returns the current status of an operation
36
+ def operation(id)
37
+ @gke.get_project_location_operation("projects/#{@project}/locations/#{@region}/operations/*", operation_id: id)
38
+ end
39
+
40
+ # operations returns a list of all operations
41
+ def operations
42
+ list = @gke.list_project_location_operations("projects/#{@project}/locations/#{@region}").operations
43
+ list.each { |x| yield x } if block_given?
44
+ list
45
+ end
46
+
47
+ # operations_by_resource returns any operations filtered by the resource
48
+ def operations_by_resource(name, resource, operation_type = '')
49
+ operations.select do |x|
50
+ next unless x.target_link.end_with?("#{resource}/#{name}")
51
+ next if !operation_type.empty? && (!x.operation_type == operation_type)
52
+
53
+ true
54
+ end
55
+ end
56
+
57
+ # hold_for_operation is responisble for waiting for an operation to complete or error
58
+ # rubocop:disable Lint/RescueException
59
+ def hold_for_operation(id, interval = 10, timeout = 900)
60
+ max_attempts = timeout / interval
61
+ retries = attempts = 0
62
+
63
+ while attempts < max_attempts
64
+ begin
65
+ resp = operation(id)
66
+ return resp if !resp.nil? && resp.status == 'DONE'
67
+ rescue Exception => e
68
+ raise Exception, "failed waiting on operation: #{id}, error: #{e}" if retries > 10
69
+
70
+ retries += 1
71
+ end
72
+ sleep(interval)
73
+ attempts += 1
74
+ end
75
+
76
+ raise Exception, "operation: #{id} has timed out waiting to finish"
77
+ end
78
+ # rubocop:enable Lint/RescueException
79
+
80
+ # cluster returns a specific cluster
81
+ def cluster(name)
82
+ return nil unless cluster?(name)
83
+
84
+ clusters.select { |x| x.name = name }.first
85
+ end
86
+
87
+ # cluster? check if a gke cluster exists
88
+ def cluster?(name)
89
+ clusters.map(&:name).include?(name)
90
+ end
91
+
92
+ # clusters returns a list of clusters
93
+ def clusters
94
+ path = "projects/#{@project}/locations/#{@region}"
95
+ list = @gke.list_zone_clusters(nil, nil, parent: path).clusters || []
96
+ list.each { |x| yield x } if block_given?
97
+ list
98
+ end
99
+
100
+ # cluster_spec is responsible for generating a cluster specification from options
101
+ # rubocop:disable Metrics/AbcSize
102
+ def cluster_spec(options)
103
+ locations = gke_locations
104
+
105
+ request = Google::Apis::ContainerV1beta1::CreateClusterRequest.new(
106
+ parent: "projects/#{@project}/locations/#{@region}",
107
+ project_id: @project
108
+ )
109
+ request.cluster = Google::Apis::ContainerV1beta1::Cluster.new(
110
+ name: options[:name],
111
+ description: options[:description],
112
+ initial_cluster_version: options[:version],
113
+
114
+ #
115
+ ## Addons
116
+ #
117
+ addons_config: Google::Apis::ContainerV1beta1::AddonsConfig.new(
118
+ cloud_run_config: Google::Apis::ContainerV1beta1::CloudRunConfig.new(
119
+ disabled: !options[:enable_cloud_run]
120
+ ),
121
+ horizontal_pod_autoscaling: Google::Apis::ContainerV1beta1::HorizontalPodAutoscaling.new(
122
+ disabled: !options[:enable_horizontal_pod_autoscaler]
123
+ ),
124
+ http_load_balancing: Google::Apis::ContainerV1beta1::HttpLoadBalancing.new(
125
+ disabled: !options[:enable_http_loadbalancer]
126
+ ),
127
+ istio_config: Google::Apis::ContainerV1beta1::IstioConfig.new(
128
+ auth: 'AUTH_MUTUAL_TLS',
129
+ disabled: !options[:enable_istio]
130
+ ),
131
+ kubernetes_dashboard: Google::Apis::ContainerV1beta1::KubernetesDashboard.new(
132
+ disabled: true
133
+ ),
134
+ network_policy_config: Google::Apis::ContainerV1beta1::NetworkPolicyConfig.new(
135
+ disabled: false
136
+ )
137
+ ),
138
+
139
+ maintenance_policy: Google::Apis::ContainerV1beta1::MaintenancePolicy.new(
140
+ window: Google::Apis::ContainerV1beta1::MaintenanceWindow.new(
141
+ daily_maintenance_window: Google::Apis::ContainerV1beta1::DailyMaintenanceWindow.new(
142
+ start_time: options[:maintenance_window]
143
+ )
144
+ )
145
+ ),
146
+
147
+ #
148
+ ## Authentication
149
+ #
150
+ master_auth: Google::Apis::ContainerV1beta1::MasterAuth.new(
151
+ client_certificate_config: Google::Apis::ContainerV1beta1::ClientCertificateConfig.new(
152
+ issue_client_certificate: false
153
+ )
154
+ ),
155
+
156
+ #
157
+ ## Network
158
+ #
159
+ ip_allocation_policy: Google::Apis::ContainerV1beta1::IpAllocationPolicy.new(
160
+ cluster_ipv4_cidr_block: options[:cluster_ipv4_cidr],
161
+ create_subnetwork: options[:create_subnetwork],
162
+ services_ipv4_cidr_block: options[:services_ipv4_cidr],
163
+ subnetwork_name: options[:subnetwork],
164
+ use_ip_aliases: true
165
+ ),
166
+ locations: locations,
167
+
168
+ #
169
+ ## Features
170
+ #
171
+ monitoring_service: ('monitoring.googleapis.com/kubernetes' if options[:enable_monitoring]),
172
+ logging_service: ('logging.googleapis.com/kubernetes' if options[:enable_logging]),
173
+
174
+ binary_authorization: Google::Apis::ContainerV1beta1::BinaryAuthorization.new(
175
+ enabled: options[:enable_binary_authorization]
176
+ ),
177
+ legacy_abac: Google::Apis::ContainerV1beta1::LegacyAbac.new(
178
+ enabled: false
179
+ ),
180
+ network_policy: Google::Apis::ContainerV1beta1::NetworkPolicy.new(
181
+ enabled: options[:enable_network_policies]
182
+ ),
183
+ pod_security_policy_config: Google::Apis::ContainerV1beta1::PodSecurityPolicyConfig.new(
184
+ enabled: options[:enable_pod_security_policies]
185
+ ),
186
+
187
+ #
188
+ ## Node Pools
189
+ #
190
+ node_pools: [
191
+ Google::Apis::ContainerV1beta1::NodePool.new(
192
+ autoscaling: Google::Apis::ContainerV1beta1::NodePoolAutoscaling.new(
193
+ autoprovisioned: false,
194
+ enabled: options[:enable_autoscaler],
195
+ max_node_count: options[:max_size],
196
+ min_node_count: options[:size]
197
+ ),
198
+ config: Google::Apis::ContainerV1beta1::NodeConfig.new(
199
+ disk_size_gb: options[:disk_size_gb],
200
+ image_type: options[:image_type],
201
+ machine_type: options[:machine_type],
202
+ oauth_scopes: [
203
+ 'https://www.googleapis.com/auth/compute',
204
+ 'https://www.googleapis.com/auth/devstorage.read_only',
205
+ 'https://www.googleapis.com/auth/logging.write',
206
+ 'https://www.googleapis.com/auth/monitoring'
207
+ ],
208
+ preemptible: options[:preemptible]
209
+ ),
210
+ initial_node_count: options[:size],
211
+ locations: locations,
212
+ management: Google::Apis::ContainerV1beta1::NodeManagement.new(
213
+ auto_repair: options[:enable_autorepair],
214
+ auto_upgrade: options[:enable_autoupgrade]
215
+ ),
216
+ max_pods_constraint: Google::Apis::ContainerV1beta1::MaxPodsConstraint.new(
217
+ max_pods_per_node: 110
218
+ ),
219
+ name: 'compute',
220
+ version: options[:version]
221
+ )
222
+ ]
223
+ )
224
+
225
+ if options[:enable_private_network]
226
+ request.cluster.private_cluster = true
227
+ request.cluster.private_cluster_config = Google::Apis::ContainerV1beta1::PrivateClusterConfig.new(
228
+ enable_private_endpoint: options[:enable_private_endpoint],
229
+ enable_private_nodes: true,
230
+ master_ipv4_cidr_block: options[:master_ipv4_cidr_block]
231
+ )
232
+
233
+ # @step: do we have any authorized cidr's
234
+ if options[:authorized_master_cidrs].size.positive?
235
+ request.cluster.master_authorized_networks_config = Google::Apis::ContainerV1beta1::MasterAuthorizedNetworksConfig.new(
236
+ cidr_blocks: [],
237
+ enabled: true
238
+ )
239
+ options[:authorized_master_cidrs].each do |x|
240
+ block = Google::Apis::ContainerV1beta1::CidrBlock.new(
241
+ cidr_block: x[:cidr],
242
+ display_name: x[:name]
243
+ )
244
+
245
+ request.cluster.master_authorized_networks_config.cidr_blocks.push(block)
246
+ end
247
+ end
248
+ end
249
+ request
250
+ end
251
+ # rubocop:enable Metrics/AbcSize
252
+ end
253
+ end
254
+ end
255
+ # rubocop:enable Metrics/LineLength,Metrics/MethodLength
256
+ end
257
+
258
+ # rubocop:disable Metrics/LineLength,Metrics/MethodLength
259
+ module HubClustersCreator
260
+ module Providers
261
+ # GCP namespaces the GCP methods
262
+ module GCP
263
+ # Compute provides some helper methods / functions to the GCP agent
264
+ module Compute
265
+ # router returns a specfic router
266
+ def router(name)
267
+ r = routers.select { |x| x.name == name }.first
268
+ yield r if block_given?
269
+ r
270
+ end
271
+
272
+ # router? check if the router exists
273
+ def router?(name)
274
+ routers.map(&:name).include?(name)
275
+ end
276
+
277
+ # routers returns the list of routers
278
+ def routers
279
+ list = @compute.list_routers(@project, @region).items
280
+ list.each { |x| yield x } if block_given?
281
+ list
282
+ end
283
+
284
+ # network? checks if the network exists in the region and project
285
+ def network?(name)
286
+ networks.items.map(&:name).include?(name)
287
+ end
288
+
289
+ # networks returns a list of networks in the region and project
290
+ def networks
291
+ list = @compute.list_networks(@project)
292
+ list.each { |x| yield x } if block_given?
293
+ list
294
+ end
295
+
296
+ # subnet? checks if the subnet exists in the project, network and region
297
+ def subnet?(name, network)
298
+ subnets(network).include?(name)
299
+ end
300
+
301
+ # dns is responsible for adding / updating a dns record in a zone
302
+ def dns(src, dest, zone, record = 'A')
303
+ raise ArgumentError, "the managed zone: #{zone} does not exist" unless domain?(zone)
304
+
305
+ hostname = "#{src}.#{zone}."
306
+ change = Google::Apis::DnsV1::Change.new(
307
+ additions: [
308
+ Google::Apis::DnsV1::ResourceRecordSet.new(
309
+ kind: 'dns#resourceRecordSet',
310
+ name: hostname,
311
+ rrdatas: [dest],
312
+ ttl: 120,
313
+ type: record
314
+ )
315
+ ]
316
+ )
317
+
318
+ # @step: check a record already exists and if so add for deletion
319
+ dns_records(zone).rrsets.each do |x|
320
+ next unless x.name == hostname
321
+
322
+ change.deletions = [x]
323
+ end
324
+
325
+ managed_zone = domain(zone)
326
+ @dns.create_change(@project, managed_zone.name, change)
327
+ end
328
+
329
+ # dns_records returns a list of dns recordsets
330
+ def dns_records(zone)
331
+ raise ArgumentError, "the managed zone: #{zone} does not exist" unless domain?(zone)
332
+
333
+ managed_zone = domain(zone)
334
+ @dns.list_resource_record_sets(@project, managed_zone.name)
335
+ end
336
+
337
+ # domain? checks if the domain exists
338
+ def domain?(name)
339
+ domains.map { |x| x.dns_name.chomp('.') }.include?(name)
340
+ end
341
+
342
+ # domain returns a specific domain
343
+ def domain(name)
344
+ domains.select { |x| x.dns_name.chomp('.') == name }.first
345
+ end
346
+
347
+ # domains provides a list of domains
348
+ def domains
349
+ @dns.list_managed_zones(@project).managed_zones
350
+ end
351
+
352
+ # subnets returns a list of subnets in the network
353
+ def subnets(network)
354
+ list = @compute.list_subnetworks(@project, @region).items.select do |x|
355
+ x.network.end_with?(network)
356
+ end.map(&:name)
357
+ list.each { |x| yield x } if block_given?
358
+ list
359
+ end
360
+ end
361
+ end
362
+ end
363
+ # rubocop:enable Metrics/LineLength,Metrics/MethodLength,Metrics/ModuleLength
364
+ end