hub-clusters-creator 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
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
+ # rubocop:disable Metrics/LineLength
19
+ module HubClustersCreator
20
+ # Errors is collection of custom errors and exceptions
21
+ module Errors
22
+ # InfrastructureError defines an error occurred creating or configuring the cluster
23
+ class InfrastructureError < StandardError
24
+ def initialize(msg = 'failed attempting to create the cluster')
25
+ super(msg)
26
+ end
27
+ end
28
+
29
+ # ConfigurationError defines an error related to configuration
30
+ class ConfigurationError < StandardError
31
+ attr_accessor :field, :value
32
+
33
+ def initialize(msg = 'invalid configuration', field:, value:)
34
+ @field = field
35
+ @value = value
36
+ super(msg)
37
+ end
38
+ end
39
+
40
+ # InitializerError is thrown when we've encountered an error attempting to bootstrap cluster
41
+ class InitializerError < StandardError
42
+ def initialize(msg = 'failed attempting to bootstrap the cluster')
43
+ super(msg)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ # rubocop:enable Metrics/LineLength
@@ -0,0 +1,147 @@
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 'k8s-client'
19
+
20
+ module HubClustersCreator
21
+ # Kube is a collection of methods for interacting with the kubernetes api
22
+ # rubocop:disable Metrics/LineLength,Metrics/MethodLength,Metrics/ParameterLists
23
+ class Kube
24
+ attr_accessor :endpoint
25
+
26
+ def initialize(endpoint, token: nil, client_certificate: nil, client_key: nil, certificate_authority: nil)
27
+ options = {
28
+ ssl_verify_peer: false
29
+ }
30
+
31
+ config = K8s::Config.new(
32
+ clusters: [{
33
+ name: 'default',
34
+ cluster: { server: 'https://' + endpoint, certificate_authority_data: certificate_authority }
35
+ }],
36
+ users: [{
37
+ name: 'default',
38
+ user: {
39
+ token: token,
40
+ client_certificate_data: client_certificate,
41
+ client_key_data: client_key
42
+ }
43
+ }],
44
+ contexts: [{
45
+ name: 'default',
46
+ context: { cluster: 'default', user: 'default' }
47
+ }],
48
+ current_context: 'default'
49
+ )
50
+
51
+ @endpoint = "https://#{endpoint}" unless endpoint.start_with?('https')
52
+ @client = K8s::Client.config(config, options)
53
+ end
54
+
55
+ # exists? checks if the resource exists
56
+ def exists?(name, kind, namespace = 'default', version = 'v1')
57
+ begin
58
+ kind = "#{kind}s" unless kind.end_with?('s')
59
+ @client.api(version).resource(kind, namespace: namespace).get(name)
60
+ rescue K8s::Error::NotFound
61
+ return false
62
+ end
63
+ true
64
+ end
65
+
66
+ # get retrieves a resource from the cluster
67
+ def get(name, namespace, kind, version: 'v1')
68
+ @client.api(version).resource(kind, namespace: namespace).get(name)
69
+ end
70
+
71
+ # delete removes a resource from the cluster
72
+ def delete(name, kind, namespace, version: 'v1')
73
+ return unless exists?(name, kind, namespace, version)
74
+
75
+ @client.api(version).resource(kind, namespace: namespace).delete_resource(name)
76
+ end
77
+
78
+ # wait is used to poll until a resource meets the needs of the consumer
79
+ # rubocop:disable Lint/RescueException,Metrics/CyclomaticComplexity,Metrics/AbcSize
80
+ def wait(name, namespace, kind, version: 'v1', max_retries: 50, timeout: 300, interval: 5, &block)
81
+ retries = counter = 0
82
+ while counter < timeout
83
+ begin
84
+ unless block_given?
85
+ return if exists?(name, kind, namespace, version)
86
+
87
+ continue
88
+ end
89
+
90
+ resource = @client.api(version).resource(kind).get(name, namespace: namespace)
91
+ return if block.call(resource)
92
+ rescue Exception => e
93
+ raise e if retries > max_retries
94
+
95
+ retries += 1
96
+ end
97
+ sleep(interval)
98
+ counter += interval
99
+ end
100
+
101
+ raise Exception, "operation waiting for #{name}/#{namespace}/#{kind} has failed"
102
+ end
103
+ # rubocop:enable Lint/RescueException,Metrics/CyclomaticComplexity,Metrics/AbcSize
104
+
105
+ # kubectl is used to apply a manifest
106
+ # rubocop:disable Metrics/AbcSize
107
+ def kubectl(manifest)
108
+ resource = K8s::Resource.from_json(YAML.safe_load(manifest).to_json)
109
+ raise ArgumentError, 'no api version associated to resource' unless resource.apiVersion
110
+ raise ArgumentError, 'no kind associated to resource' unless resource.kind
111
+ raise ArgumentError, 'no metadata associated to resource' unless resource.metadata
112
+ raise ArgumentError, 'no name associated to resource' unless resource.metadata.name
113
+
114
+ name = resource.metadata.name
115
+ namespace = resource.metadata.namespace
116
+ kind = resource.kind.downcase
117
+ version = resource.apiVersion
118
+ return if exists?(name, kind, namespace, version)
119
+
120
+ @client.api(version).resource("#{kind}s", namespace: namespace).create_resource(resource)
121
+ end
122
+ # rubocop:enable Metrics/AbcSize
123
+
124
+ # account returns the credentials for a service account
125
+ def account(name, namespace = 'kube-system')
126
+ sa = @client.api('v1').resource('serviceaccounts', namespace: namespace).get(name)
127
+ secret = @client.api('v1').resource('secrets', namespace: namespace).get(sa.secrets.first.name)
128
+ secret.data.token
129
+ end
130
+
131
+ # wait_for_kubeapi is responsible for waiting the api is available
132
+ def wait_for_kubeapi(max_attempts = 60, interval = 5)
133
+ attempts = 0
134
+ while attempts < max_attempts
135
+ begin
136
+ return if @client.api('v1').resource('nodes').list
137
+ rescue StandardError => e
138
+ puts "bad: #{e}"
139
+ attempts += 1
140
+ end
141
+ sleep(interval)
142
+ end
143
+ raise Exception, 'timed out waiting for the kube api'
144
+ end
145
+ end
146
+ # rubocop:enable Metrics/LineLength,Metrics/MethodLength,Metrics/ParameterLists
147
+ end
@@ -0,0 +1,46 @@
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
+ module HubClustersCreator
20
+ # Logging is few helper functions for logging
21
+ module Logging
22
+ def info(string, options = {})
23
+ print formatted_string("[info] #{dated_string(string)}", options)
24
+ end
25
+
26
+ def warn(string)
27
+ Kernel.warn formatted_string(string, symbol: '*')
28
+ end
29
+
30
+ def error(string)
31
+ Kernel.warn formatted_string(string, symbol: '!')
32
+ end
33
+
34
+ private
35
+
36
+ def dated_string(string)
37
+ "[#{Time.now}] #{string}"
38
+ end
39
+
40
+ def formatted_string(string, options = {})
41
+ symbol = options[:symbol] || ''
42
+ string = string.to_s
43
+ "#{symbol}#{string}\n"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,272 @@
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 'hub-clusters-creator/providers/aks/helpers'
19
+ require 'hub-clusters-creator/providers/bootstrap'
20
+ require 'hub-clusters-creator/template'
21
+
22
+ require 'azure_mgmt_resources'
23
+ require 'azure_mgmt_container_service'
24
+ require 'azure_mgmt_dns'
25
+ require 'uri'
26
+
27
+ # rubocop:disable Metrics/ClassLength,Metrics/LineLength,Metrics/MethodLength
28
+ module HubClustersCreator
29
+ module Providers
30
+ # AKS is the AKS provider
31
+ class AKS
32
+ include ::Azure::Resources::Profiles::Latest::Mgmt
33
+ include ::Azure::Resources::Profiles::Latest::Mgmt::Models
34
+ include ::Azure::ContainerService::Mgmt::V2019_04_01
35
+ include ::Azure::Dns::Mgmt::V2017_10_01
36
+ include Azure::Helpers
37
+ include HubClustersCreator::Utils::Template
38
+ include Errors
39
+ include Logging
40
+
41
+ # rubocop:disable Metrics/AbcSize
42
+ def initialize(provider)
43
+ %i[client_id client_secret region subscription tenant].each do |x|
44
+ raise ArgumentError, "you must specify the '#{x}' provider option" unless provider.key?(x)
45
+ end
46
+
47
+ @subscription = provider[:subscription]
48
+ @tenant = provider[:tenant]
49
+ @client_id = provider[:client_id]
50
+ @client_secret = provider[:client_secret]
51
+ @region = provider[:region]
52
+
53
+ @provider = MsRestAzure::ApplicationTokenProvider.new(@tenant, @client_id, @client_secret)
54
+ @credentials = MsRest::TokenCredentials.new(@provider)
55
+
56
+ @containers = ::Azure::ContainerService::Mgmt::V2019_04_01::ContainerServiceClient.new(@credentials)
57
+ @containers.subscription_id = @subscription
58
+
59
+ @dns = ::Azure::Dns::Mgmt::V2017_10_01::DnsManagementClient.new(@credentials)
60
+ @dns.subscription_id = @subscription
61
+
62
+ options = {
63
+ client_id: @client_id,
64
+ client_secret: @client_secret,
65
+ credentials: @credentials,
66
+ subscription_id: @subscription,
67
+ tenant_id: @tenant
68
+ }
69
+
70
+ @client = Client.new(options)
71
+ end
72
+ # rubocop:enable Metrics/AbcSize
73
+
74
+ # create is responsible for creating the cluster
75
+ def create(name, config)
76
+ # @step: validate the user defined options
77
+ validate(config)
78
+
79
+ # @step: create the infrastructure deployment
80
+ begin
81
+ provision_aks(name, config)
82
+ rescue StandardError => e
83
+ raise InfrastructureError, "failed to provision cluster, error: #{e}"
84
+ end
85
+
86
+ # @step: bootstrap the cluster
87
+ begin
88
+ provision_cluster(name, config)
89
+ rescue StandardError => e
90
+ raise InfrastructureError, "failed to bootstrap cluster, error: #{e}"
91
+ end
92
+ end
93
+
94
+ # delete is responsible for deleting the cluster via resource group
95
+ def delete(name)
96
+ return unless resource_group?(name)
97
+
98
+ info "deleting the resource group: #{name}"
99
+ @client.resource_groups.delete(name, name)
100
+ end
101
+
102
+ private
103
+
104
+ # provision_aks is responsible for provision the infrastructure
105
+ # rubocop:disable Metrics/AbcSize
106
+ def provision_aks(name, config)
107
+ # @step: define the resource group
108
+ resource_group_name = name
109
+
110
+ # @step: check the resource group exists
111
+ if resource_group?(resource_group_name)
112
+ info "skipping the resource group creation: #{resource_group_name}, already exists"
113
+ else
114
+ info "creating the resource group: #{resource_group_name} in azure"
115
+ params = ::Azure::Resources::Mgmt::V2019_05_10::Models::ResourceGroup.new.tap do |x|
116
+ x.location = @region
117
+ end
118
+ # ensure the resource group is created
119
+ @client.resource_groups.create_or_update(resource_group_name, params)
120
+
121
+ # wait for the resource group to be created
122
+ wait(max_retries: 20, interval: 10) do
123
+ resource_group?(resource_group_name)
124
+ end
125
+ end
126
+
127
+ info "provisioning the azure deployment manifest: '#{name}', resource group: '#{resource_group_name}'"
128
+ # @step: generate the ARM deployments
129
+ template = YAML.safe_load(cluster_template(config))
130
+
131
+ # @step: check if a deployment is already underway and wait for completion - which
132
+ # makes it eaisier to rerun quickly
133
+ if deployment?(resource_group_name, name)
134
+ info "deployment: #{name}, resource group: #{resource_group_name} already underway, waiting for completion"
135
+ wait(interval: 30, max_retries: 20) do
136
+ if deployment?(resource_group_name, name)
137
+ d = deployment(resource_group_name, name)
138
+ d.properties.provisioning_state == 'Succeeded'
139
+ end
140
+ end
141
+ end
142
+
143
+ # @step: kick off the deployment and cross fingers
144
+ deployment = ::Azure::Resources::Mgmt::V2019_05_10::Models::Deployment.new
145
+ deployment.properties = ::Azure::Resources::Mgmt::V2019_05_10::Models::DeploymentProperties.new
146
+ deployment.properties.template = template
147
+ deployment.properties.mode = ::Azure::Resources::Mgmt::V2019_05_10::Models::DeploymentMode::Incremental
148
+
149
+ # put the deployment to the resource group
150
+ @client.deployments.create_or_update_async(resource_group_name, name, deployment)
151
+ # wait for the deployment to finish
152
+ wait(interval: 30, max_retries: 20) do
153
+ if deployment?(resource_group_name, name)
154
+ d = deployment(resource_group_name, name)
155
+ d.properties.provisioning_state == 'Succeeded'
156
+ end
157
+ end
158
+ end
159
+ # rubocop:enable Metrics/AbcSize
160
+
161
+ # provision_cluster is responsible for kicking off the initialization
162
+ # rubocop:disable Metrics/AbcSize
163
+ def provision_cluster(name, config)
164
+ resource_group_name = name
165
+
166
+ # @step retrieve the kubeconfig - I HATE everything about Azure!!
167
+ packed = @containers.managed_clusters.list_cluster_admin_credentials(resource_group_name, name)
168
+ kc = YAML.safe_load(packed.kubeconfigs.first.value.pack('c*'))
169
+
170
+ ca = kc['clusters'].first['cluster']['certificate-authority-data']
171
+ endpoint = URI(kc['clusters'].first['cluster']['server']).hostname
172
+
173
+ # @step: provision a kubernetes client for this cluster
174
+ kube = HubClustersCreator::Kube.new(endpoint,
175
+ client_certificate: kc['users'].first['user']['client-certificate-data'],
176
+ client_key: kc['users'].first['user']['client-key-data'])
177
+
178
+ info "waiting for the kubeapi to become available at: #{endpoint}"
179
+ kube.wait_for_kubeapi
180
+
181
+ # @step: provision the bootstrap
182
+ info "attempting to bootstrap the cluster: #{name}"
183
+ HubClustersCreator::Providers::Bootstrap.new(name, kube, config).bootstrap
184
+
185
+ # @step: update the dns record for the ingress
186
+ unless (config[:grafana_hostname] || '').empty?
187
+ # Get the ingress resource and extract the load balancer ip address
188
+ ingress = @client.get('loki-grafana', 'loki', 'ingresses', version: 'extensions/v1beta1')
189
+
190
+ unless ingress.status.loadBalancer.ingress.empty?
191
+ address = ingress.status.loadBalancer.ingress.first.ip
192
+ info "adding a dns record for #{config[:grafana_hostname]} => #{address}"
193
+ dns(hostname(config[:grafana_hostname]), address, config[:domain])
194
+ end
195
+ end
196
+
197
+ {
198
+ cluster: {
199
+ ca: ca,
200
+ endpoint: "https://#{endpoint}",
201
+ token: kube.account('sysadmin')
202
+ },
203
+ config: config,
204
+ services: {
205
+ grafana: {
206
+ hostname: config[:grafana_hostname]
207
+ }
208
+ }
209
+ }
210
+ end
211
+ # rubocop:enable Metrics/AbcSize
212
+
213
+ # validate is responsible for validating the options
214
+ def validate(options); end
215
+
216
+ # cluster_template is responsible for rendering the template for ARM
217
+ def cluster_template(config)
218
+ template = <<~YAML
219
+ '$schema': https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#
220
+ contentVersion: 1.0.0.0
221
+ parameters: {}
222
+ variables: {}
223
+ resources:
224
+ - type: Microsoft.ContainerService/managedClusters
225
+ name: <%= context[:name] %>
226
+ apiVersion: '2019-06-01'
227
+ location: #{@region}
228
+ tags:
229
+ cluster: <%= context[:name] %>
230
+ properties:
231
+ kubernetesVersion: <%= context[:version] %>
232
+ dnsPrefix: <%= context[:name] %>
233
+ addonProfiles:
234
+ httpapplicationrouting:
235
+ enabled: true
236
+ config:
237
+ HTTPApplicationRoutingZoneName: <%= context[:domain] %>
238
+ agentPoolProfiles:
239
+ - name: compute
240
+ count: <%= context[:size] %>
241
+ maxPods: 110
242
+ osDiskSizeGB: <%= context[:disk_size_gb] %>
243
+ osType: Linux
244
+ storageProfile: ManagedDisks
245
+ type: VirtualMachineScaleSets
246
+ vmSize: <%= context[:machine_type] %>
247
+ servicePrincipalProfile:
248
+ clientId: #{@client_id}
249
+ secret: #{@client_secret}
250
+ linuxProfile:
251
+ adminUsername: azureuser
252
+ <%- unless (context[:ssh_key] || '').empty? -%>
253
+ ssh:
254
+ publicKeys:
255
+ - keyData: <%= context[:ssh_key] %>
256
+ <%- end -%>
257
+ enableRBAC: true
258
+ enablePodSecurityPolicy: true
259
+ networkProfile:
260
+ dnsServiceIP: 10.0.0.10
261
+ dockerBridgeCidr: 172.17.0.1/16
262
+ loadBalancerSku: basic
263
+ networkPlugin: azure
264
+ networkPolicy: azure
265
+ serviceCidr: <%= context[:services_ipv4_cidr].empty? ? '10.0.0.0/16' : context[:services_ipv4_cidr] %>
266
+ YAML
267
+ HubClustersCreator::Utils::Template::Render.new(config).render(template)
268
+ end
269
+ end
270
+ end
271
+ end
272
+ # rubocop:enable Metrics/ClassLength,Metrics/LineLength,Metrics/MethodLength