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