hetzner-k3s 0.6.2.pre1 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,475 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../utils'
4
+
5
+ module Kubernetes
6
+ class Client
7
+ include Utils
8
+
9
+ def initialize(configuration:)
10
+ @configuration = configuration
11
+ end
12
+
13
+ def deploy(masters:, workers:, master_definitions:, worker_definitions:)
14
+ @masters = masters
15
+ @workers = workers
16
+ @master_definitions = master_definitions
17
+ @worker_definitions = worker_definitions
18
+
19
+ @kube_api_server_args = configuration.fetch('kube_api_server_args', [])
20
+ @kube_scheduler_args = configuration.fetch('kube_scheduler_args', [])
21
+ @kube_controller_manager_args = configuration.fetch('kube_controller_manager_args', [])
22
+ @kube_cloud_controller_manager_args = configuration.fetch('kube_cloud_controller_manager_args', [])
23
+ @kubelet_args = configuration.fetch('kubelet_args', [])
24
+ @kube_proxy_args = configuration.fetch('kube_proxy_args', [])
25
+ @private_ssh_key_path = File.expand_path(configuration['private_ssh_key_path'])
26
+ @public_ssh_key_path = File.expand_path(configuration['public_ssh_key_path'])
27
+ @cluster_name = configuration['cluster_name']
28
+
29
+ set_up_k3s
30
+
31
+ update_nodes
32
+
33
+ post_setup_deployments
34
+ end
35
+
36
+ def upgrade
37
+ worker_upgrade_concurrency = workers.size - 1
38
+ worker_upgrade_concurrency = 1 if worker_upgrade_concurrency.zero?
39
+
40
+ cmd = <<~BASH
41
+ kubectl apply -f - <<-EOF
42
+ apiVersion: upgrade.cattle.io/v1
43
+ kind: Plan
44
+ metadata:
45
+ name: k3s-server
46
+ namespace: system-upgrade
47
+ labels:
48
+ k3s-upgrade: server
49
+ spec:
50
+ concurrency: 1
51
+ version: #{new_k3s_version}
52
+ nodeSelector:
53
+ matchExpressions:
54
+ - {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
55
+ serviceAccountName: system-upgrade
56
+ tolerations:
57
+ - key: "CriticalAddonsOnly"
58
+ operator: "Equal"
59
+ value: "true"
60
+ effect: "NoExecute"
61
+ cordon: true
62
+ upgrade:
63
+ image: rancher/k3s-upgrade
64
+ EOF
65
+ BASH
66
+
67
+ run cmd, kubeconfig_path: kubeconfig_path
68
+
69
+ cmd = <<~BASH
70
+ kubectl apply -f - <<-EOF
71
+ apiVersion: upgrade.cattle.io/v1
72
+ kind: Plan
73
+ metadata:
74
+ name: k3s-agent
75
+ namespace: system-upgrade
76
+ labels:
77
+ k3s-upgrade: agent
78
+ spec:
79
+ concurrency: #{worker_upgrade_concurrency}
80
+ version: #{new_k3s_version}
81
+ nodeSelector:
82
+ matchExpressions:
83
+ - {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
84
+ serviceAccountName: system-upgrade
85
+ prepare:
86
+ image: rancher/k3s-upgrade
87
+ args: ["prepare", "k3s-server"]
88
+ cordon: true
89
+ upgrade:
90
+ image: rancher/k3s-upgrade
91
+ EOF
92
+ BASH
93
+
94
+ run cmd, kubeconfig_path: kubeconfig_path
95
+
96
+ puts 'Upgrade will now start. Run `watch kubectl get nodes` to see the nodes being upgraded. This should take a few minutes for a small cluster.'
97
+ puts 'The API server may be briefly unavailable during the upgrade of the controlplane.'
98
+
99
+ updated_configuration = configuration.raw
100
+ updated_configuration['k3s_version'] = new_k3s_version
101
+
102
+ File.write(config_file, updated_configuration.to_yaml)
103
+ end
104
+
105
+ private
106
+
107
+ attr_reader :configuration, :masters, :workers, :kube_api_server_args, :kube_scheduler_args,
108
+ :kube_controller_manager_args, :kube_cloud_controller_manager_args, :kubelet_args, :kube_proxy_args,
109
+ :private_ssh_key_path, :public_ssh_key_path, :master_definitions, :worker_definitions, :cluster_name
110
+
111
+ def set_up_k3s
112
+ set_up_first_master
113
+ set_up_additional_masters
114
+ set_up_workers
115
+ end
116
+
117
+ def set_up_first_master
118
+ puts
119
+ puts "Deploying k3s to first master (#{first_master['name']})..."
120
+
121
+ ssh first_master, master_install_script(first_master), print_output: true
122
+
123
+ puts
124
+ puts 'Waiting for the control plane to be ready...'
125
+
126
+ sleep 10
127
+
128
+ puts
129
+ puts '...k3s has been deployed to first master.'
130
+
131
+ save_kubeconfig
132
+ end
133
+
134
+ def set_up_additional_masters
135
+ return unless masters.size > 1
136
+
137
+ threads = masters[1..].map do |master|
138
+ Thread.new do
139
+ puts
140
+ puts "Deploying k3s to master #{master['name']}..."
141
+
142
+ ssh master, master_install_script(master), print_output: true
143
+
144
+ puts
145
+ puts "...k3s has been deployed to master #{master['name']}."
146
+ end
147
+ end
148
+
149
+ threads.each(&:join) unless threads.empty?
150
+ end
151
+
152
+ def set_up_workers
153
+ threads = workers.map do |worker|
154
+ Thread.new do
155
+ puts
156
+ puts "Deploying k3s to worker (#{worker['name']})..."
157
+
158
+ ssh worker, worker_install_script(worker), print_output: true
159
+
160
+ puts
161
+ puts "...k3s has been deployed to worker (#{worker['name']})."
162
+ end
163
+ end
164
+
165
+ threads.each(&:join) unless threads.empty?
166
+ end
167
+
168
+ def post_setup_deployments
169
+ deploy_cloud_controller_manager
170
+ deploy_csi_driver
171
+ deploy_system_upgrade_controller
172
+ end
173
+
174
+ def update_nodes
175
+ mark_nodes mark_type: :labels
176
+ mark_nodes mark_type: :taints
177
+ end
178
+
179
+ def first_master
180
+ masters.first
181
+ end
182
+
183
+ def kube_api_server_args_list
184
+ return '' if kube_api_server_args.empty?
185
+
186
+ kube_api_server_args.map do |arg|
187
+ " --kube-apiserver-arg=\"#{arg}\" "
188
+ end.join
189
+ end
190
+
191
+ def kube_scheduler_args_list
192
+ return '' if kube_scheduler_args.empty?
193
+
194
+ kube_scheduler_args.map do |arg|
195
+ " --kube-scheduler-arg=\"#{arg}\" "
196
+ end.join
197
+ end
198
+
199
+ def kube_controller_manager_args_list
200
+ return '' if kube_controller_manager_args.empty?
201
+
202
+ kube_controller_manager_args.map do |arg|
203
+ " --kube-controller-manager-arg=\"#{arg}\" "
204
+ end.join
205
+ end
206
+
207
+ def kube_cloud_controller_manager_args_list
208
+ return '' if kube_cloud_controller_manager_args.empty?
209
+
210
+ kube_cloud_controller_manager_args.map do |arg|
211
+ " --kube-cloud-controller-manager-arg=\"#{arg}\" "
212
+ end.join
213
+ end
214
+
215
+ def kubelet_args_list
216
+ return '' if kubelet_args.empty?
217
+
218
+ kubelet_args.map do |arg|
219
+ " --kubelet-arg=\"#{arg}\" "
220
+ end.join
221
+ end
222
+
223
+ def kube_proxy_args_list
224
+ return '' if kube_proxy_args.empty?
225
+
226
+ kube_api_server_args.map do |arg|
227
+ " --kube-proxy-arg=\"#{arg}\" "
228
+ end.join
229
+ end
230
+
231
+ def api_server_ip
232
+ return @api_server_ip if @api_server_ip
233
+
234
+ @api_server_ip = if masters.size > 1
235
+ load_balancer_name = "#{cluster_name}-api"
236
+ load_balancer = hetzner_client.get('/load_balancers')['load_balancers'].detect do |lb|
237
+ lb['name'] == load_balancer_name
238
+ end
239
+ load_balancer['public_net']['ipv4']['ip']
240
+ else
241
+ first_master_public_ip
242
+ end
243
+ end
244
+
245
+ def master_install_script(master)
246
+ server = master == first_master ? ' --cluster-init ' : " --server https://#{api_server_ip}:6443 "
247
+ flannel_interface = find_flannel_interface(master)
248
+ enable_encryption = configuration.fetch('enable_encryption', false)
249
+ flannel_wireguard = if enable_encryption
250
+ if Gem::Version.new(k3s_version.scan(/\Av(.*)\+.*\Z/).flatten.first) >= Gem::Version.new('1.23.6')
251
+ ' --flannel-backend=wireguard-native '
252
+ else
253
+ ' --flannel-backend=wireguard '
254
+ end
255
+ else
256
+ ' '
257
+ end
258
+
259
+ extra_args = "#{kube_api_server_args_list} #{kube_scheduler_args_list} #{kube_controller_manager_args_list} #{kube_cloud_controller_manager_args_list} #{kubelet_args_list} #{kube_proxy_args_list}"
260
+ taint = schedule_workloads_on_masters? ? ' ' : ' --node-taint CriticalAddonsOnly=true:NoExecute '
261
+
262
+ <<~SCRIPT
263
+ curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
264
+ --disable-cloud-controller \
265
+ --disable servicelb \
266
+ --disable traefik \
267
+ --disable local-storage \
268
+ --disable metrics-server \
269
+ --write-kubeconfig-mode=644 \
270
+ --node-name="$(hostname -f)" \
271
+ --cluster-cidr=10.244.0.0/16 \
272
+ --etcd-expose-metrics=true \
273
+ #{flannel_wireguard} \
274
+ --kube-controller-manager-arg="bind-address=0.0.0.0" \
275
+ --kube-proxy-arg="metrics-bind-address=0.0.0.0" \
276
+ --kube-scheduler-arg="bind-address=0.0.0.0" \
277
+ #{taint} #{extra_args} \
278
+ --kubelet-arg="cloud-provider=external" \
279
+ --advertise-address=$(hostname -I | awk '{print $2}') \
280
+ --node-ip=$(hostname -I | awk '{print $2}') \
281
+ --node-external-ip=$(hostname -I | awk '{print $1}') \
282
+ --flannel-iface=#{flannel_interface} \
283
+ #{server} #{tls_sans}" sh -
284
+ SCRIPT
285
+ end
286
+
287
+ def worker_install_script(worker)
288
+ flannel_interface = find_flannel_interface(worker)
289
+
290
+ <<~BASH
291
+ curl -sfL https://get.k3s.io | K3S_TOKEN="#{k3s_token}" INSTALL_K3S_VERSION="#{k3s_version}" K3S_URL=https://#{first_master_private_ip}:6443 INSTALL_K3S_EXEC="agent \
292
+ --node-name="$(hostname -f)" \
293
+ --kubelet-arg="cloud-provider=external" \
294
+ --node-ip=$(hostname -I | awk '{print $2}') \
295
+ --node-external-ip=$(hostname -I | awk '{print $1}') \
296
+ --flannel-iface=#{flannel_interface}" sh -
297
+ BASH
298
+ end
299
+
300
+ def find_flannel_interface(server)
301
+ if ssh(server, 'lscpu | grep Vendor') =~ /Intel/
302
+ 'ens10'
303
+ else
304
+ 'enp7s0'
305
+ end
306
+ end
307
+
308
+ def hetzner_client
309
+ configuration.hetzner_client
310
+ end
311
+
312
+ def first_master_public_ip
313
+ @first_master_public_ip ||= first_master.dig('public_net', 'ipv4', 'ip')
314
+ end
315
+
316
+ def save_kubeconfig
317
+ kubeconfig = ssh(first_master, 'cat /etc/rancher/k3s/k3s.yaml')
318
+ .gsub('127.0.0.1', api_server_ip)
319
+ .gsub('default', configuration['cluster_name'])
320
+
321
+ File.write(kubeconfig_path, kubeconfig)
322
+
323
+ FileUtils.chmod 'go-r', kubeconfig_path
324
+ end
325
+
326
+ def kubeconfig_path
327
+ @kubeconfig_path ||= File.expand_path(configuration['kubeconfig_path'])
328
+ end
329
+
330
+ def schedule_workloads_on_masters?
331
+ schedule_workloads_on_masters = configuration['schedule_workloads_on_masters']
332
+ schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
333
+ end
334
+
335
+ def k3s_version
336
+ @k3s_version ||= configuration['k3s_version']
337
+ end
338
+
339
+ def k3s_token
340
+ @k3s_token ||= begin
341
+ token = ssh(first_master, '{ TOKEN=$(< /var/lib/rancher/k3s/server/node-token); } 2> /dev/null; echo $TOKEN')
342
+
343
+ if token.empty?
344
+ SecureRandom.hex
345
+ else
346
+ token.split(':').last
347
+ end
348
+ end
349
+ end
350
+
351
+ def tls_sans
352
+ sans = " --tls-san=#{api_server_ip} "
353
+
354
+ masters.each do |master|
355
+ master_private_ip = master['private_net'][0]['ip']
356
+ sans += " --tls-san=#{master_private_ip} "
357
+ end
358
+
359
+ sans
360
+ end
361
+
362
+ def mark_nodes(mark_type:)
363
+ check_kubectl
364
+
365
+ action = mark_type == :labels ? 'label' : 'taint'
366
+
367
+ if master_definitions.first[mark_type]
368
+ master_labels = master_definitions.first[mark_type].map { |k, v| "#{k}=#{v}" }.join(' ')
369
+ master_node_names = []
370
+
371
+ master_definitions.each do |master|
372
+ master_node_names << "#{configuration['cluster_name']}-#{master[:instance_type]}-#{master[:instance_id]}"
373
+ end
374
+
375
+ master_node_names = master_node_names.join(' ')
376
+
377
+ cmd = "kubectl #{action} --overwrite nodes #{master_node_names} #{master_labels}"
378
+
379
+ run cmd, kubeconfig_path: kubeconfig_path
380
+ end
381
+
382
+ return unless worker_definitions.any?
383
+
384
+ worker_definitions.each do |worker|
385
+ next unless worker[mark_type]
386
+
387
+ worker_labels = worker[mark_type].map { |k, v| "#{k}=#{v}" }.join(' ')
388
+ worker_node_name = "#{configuration['cluster_name']}-#{worker[:instance_type]}-#{worker[:instance_id]}"
389
+
390
+ cmd = "kubectl #{action} --overwrite nodes #{worker_node_name} #{worker_labels}"
391
+
392
+ run cmd, kubeconfig_path: kubeconfig_path
393
+ end
394
+ end
395
+
396
+ def deploy_cloud_controller_manager
397
+ check_kubectl
398
+
399
+ puts
400
+ puts 'Deploying Hetzner Cloud Controller Manager...'
401
+
402
+ cmd = <<~BASH
403
+ kubectl apply -f - <<-EOF
404
+ apiVersion: "v1"
405
+ kind: "Secret"
406
+ metadata:
407
+ namespace: 'kube-system'
408
+ name: 'hcloud'
409
+ stringData:
410
+ network: "#{configuration['existing_network'] || cluster_name}"
411
+ token: "#{configuration.hetzner_token}"
412
+ EOF
413
+ BASH
414
+
415
+ run cmd, kubeconfig_path: kubeconfig_path
416
+
417
+ cmd = 'kubectl apply -f https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml'
418
+
419
+ run cmd, kubeconfig_path: kubeconfig_path
420
+
421
+ puts '...Cloud Controller Manager deployed'
422
+ end
423
+
424
+ def deploy_system_upgrade_controller
425
+ check_kubectl
426
+
427
+ puts
428
+ puts 'Deploying k3s System Upgrade Controller...'
429
+
430
+ cmd = 'kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.9.1/system-upgrade-controller.yaml'
431
+
432
+ run cmd, kubeconfig_path: kubeconfig_path
433
+
434
+ puts '...k3s System Upgrade Controller deployed'
435
+ end
436
+
437
+ def deploy_csi_driver
438
+ check_kubectl
439
+
440
+ puts
441
+ puts 'Deploying Hetzner CSI Driver...'
442
+
443
+ cmd = <<~BASH
444
+ kubectl apply -f - <<-EOF
445
+ apiVersion: "v1"
446
+ kind: "Secret"
447
+ metadata:
448
+ namespace: 'kube-system'
449
+ name: 'hcloud-csi'
450
+ stringData:
451
+ token: "#{configuration.hetzner_token}"
452
+ EOF
453
+ BASH
454
+
455
+ run cmd, kubeconfig_path: kubeconfig_path
456
+
457
+ cmd = 'kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/master/deploy/kubernetes/hcloud-csi.yml'
458
+
459
+ run cmd, kubeconfig_path: kubeconfig_path
460
+
461
+ puts '...CSI Driver deployed'
462
+ end
463
+
464
+ def check_kubectl
465
+ return if which('kubectl')
466
+
467
+ puts 'Please ensure kubectl is installed and in your PATH.'
468
+ exit 1
469
+ end
470
+
471
+ def first_master_private_ip
472
+ @first_master_private_ip ||= first_master['private_net'][0]['ip']
473
+ end
474
+ end
475
+ end
data/lib/hetzner/utils.rb CHANGED
@@ -69,6 +69,7 @@ module Utils
69
69
  end
70
70
 
71
71
  def ssh(server, command, print_output: false)
72
+ debug = ENV.fetch('SSH_DEBUG', false)
72
73
  retries = 0
73
74
 
74
75
  public_ip = server.dig('public_net', 'ipv4', 'ip')
@@ -77,6 +78,7 @@ module Utils
77
78
  params = { verify_host_key: (verify_host_key ? :always : :never) }
78
79
 
79
80
  params[:keys] = private_ssh_key_path && [private_ssh_key_path]
81
+ params[:verbose] = :debug if debug
80
82
 
81
83
  Net::SSH.start(public_ip, 'root', params) do |session|
82
84
  session.exec!(command) do |_channel, _stream, data|
@@ -85,20 +87,24 @@ module Utils
85
87
  end
86
88
  end
87
89
  output.chop
88
- # rescue StandardError => e
89
- # p [e.class, e.message]
90
- # retries += 1
91
- # retry unless retries > 15 || e.message =~ /Bad file descriptor/
90
+ rescue Timeout::Error, IOError, Errno::EBADF => e
91
+ puts "SSH CONNECTION DEBUG: #{e.message}" if debug
92
+ retries += 1
93
+ retry unless retries > 15
92
94
  rescue Net::SSH::Disconnect => e
95
+ puts "SSH CONNECTION DEBUG: #{e.message}" if debug
93
96
  retries += 1
94
97
  retry unless retries > 15 || e.message =~ /Too many authentication failures/
95
- rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
98
+ rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH => e
99
+ puts "SSH CONNECTION DEBUG: #{e.message}" if debug
96
100
  retries += 1
97
101
  retry if retries <= 15
98
- rescue Net::SSH::AuthenticationFailed
102
+ rescue Net::SSH::AuthenticationFailed => e
103
+ puts "SSH CONNECTION DEBUG: #{e.message}" if debug
99
104
  puts '\nCannot continue: SSH authentication failed. Please ensure that the private SSH key is correct.'
100
105
  exit 1
101
- rescue Net::SSH::HostKeyMismatch
106
+ rescue Net::SSH::HostKeyMismatch => e
107
+ puts "SSH CONNECTION DEBUG: #{e.message}" if debug
102
108
  puts <<-MESSAGE
103
109
  Cannot continue: Unable to SSH into server with IP #{public_ip} because the existing fingerprint in the known_hosts file does not match that of the actual host key.\n
104
110
  This is due to a security check but can also happen when creating a new server that gets assigned the same IP address as another server you've owned in the past.\n
@@ -106,4 +112,8 @@ module Utils
106
112
  MESSAGE
107
113
  exit 1
108
114
  end
115
+
116
+ def verify_host_key
117
+ @verify_host_key ||= configuration.fetch('verify_host_key', false)
118
+ end
109
119
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hetzner-k3s
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2.pre1
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vito Botta
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-28 00:00:00.000000000 Z
11
+ date: 2022-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt_pbkdf
@@ -70,16 +70,16 @@ dependencies:
70
70
  name: net-ssh
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: 6.0.2
75
+ version: '0'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: 6.0.2
82
+ version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: sshkey
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -155,6 +155,7 @@ files:
155
155
  - lib/hetzner/k3s/cluster.rb
156
156
  - lib/hetzner/k3s/configuration.rb
157
157
  - lib/hetzner/k3s/version.rb
158
+ - lib/hetzner/kubernetes/client.rb
158
159
  - lib/hetzner/utils.rb
159
160
  - spec/k3s_spec.rb
160
161
  - spec/spec_helper.rb
@@ -177,9 +178,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
177
178
  version: 2.7.1
178
179
  required_rubygems_version: !ruby/object:Gem::Requirement
179
180
  requirements:
180
- - - ">"
181
+ - - ">="
181
182
  - !ruby/object:Gem::Version
182
- version: 1.3.1
183
+ version: '0'
183
184
  requirements: []
184
185
  rubygems_version: 3.1.2
185
186
  signing_key: