hetzner-k3s 0.5.8 → 0.6.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
- require 'http'
4
+ require 'openssl'
5
+ require 'httparty'
5
6
  require 'sshkey'
6
7
  require 'ipaddr'
7
8
  require 'open-uri'
@@ -27,14 +28,14 @@ module Hetzner
27
28
  option :config_file, required: true
28
29
  def create_cluster
29
30
  configuration.validate action: :create
30
- Cluster.new(configuration:).create
31
+ Cluster.new(configuration: configuration).create
31
32
  end
32
33
 
33
34
  desc 'delete-cluster', 'Delete an existing k3s cluster in Hetzner Cloud'
34
35
  option :config_file, required: true
35
36
  def delete_cluster
36
37
  configuration.validate action: :delete
37
- Cluster.new(configuration:).delete
38
+ Cluster.new(configuration: configuration).delete
38
39
  end
39
40
 
40
41
  desc 'upgrade-cluster', 'Upgrade an existing k3s cluster in Hetzner Cloud to a new version'
@@ -43,7 +44,7 @@ module Hetzner
43
44
  option :force, default: 'false'
44
45
  def upgrade_cluster
45
46
  configuration.validate action: :upgrade
46
- Cluster.new(configuration:).upgrade(new_k3s_version: options[:new_k3s_version], config_file: options[:config_file])
47
+ Cluster.new(configuration: configuration).upgrade(new_k3s_version: options[:new_k3s_version], config_file: options[:config_file])
47
48
  end
48
49
 
49
50
  desc 'releases', 'List available k3s releases'
@@ -59,7 +60,7 @@ module Hetzner
59
60
 
60
61
  def configuration
61
62
  @configuration ||= begin
62
- config = ::Hetzner::Configuration.new(options:)
63
+ config = ::Hetzner::Configuration.new(options: options)
63
64
  @hetzner_token = config.hetzner_token
64
65
  config
65
66
  end
@@ -4,7 +4,7 @@ require 'net/ssh'
4
4
  require 'securerandom'
5
5
  require 'base64'
6
6
  require 'timeout'
7
- require 'subprocess'
7
+ require 'fileutils'
8
8
 
9
9
  require_relative '../infra/client'
10
10
  require_relative '../infra/firewall'
@@ -35,7 +35,8 @@ class Cluster
35
35
  @masters_location = configuration['location']
36
36
  @verify_host_key = configuration.fetch('verify_host_key', false)
37
37
  @servers = []
38
- @networks = configuration['ssh_allowed_networks']
38
+ @ssh_networks = configuration['ssh_allowed_networks']
39
+ @api_networks = configuration['api_allowed_networks']
39
40
  @enable_encryption = configuration.fetch('enable_encryption', false)
40
41
  @kube_api_server_args = configuration.fetch('kube_api_server_args', [])
41
42
  @kube_scheduler_args = configuration.fetch('kube_scheduler_args', [])
@@ -82,17 +83,17 @@ class Cluster
82
83
  :masters_config, :worker_node_pools,
83
84
  :masters_location, :public_ssh_key_path,
84
85
  :hetzner_token, :new_k3s_version,
85
- :config_file, :verify_host_key, :networks, :private_ssh_key_path,
86
+ :config_file, :verify_host_key, :ssh_networks, :private_ssh_key_path,
86
87
  :enable_encryption, :kube_api_server_args, :kube_scheduler_args,
87
88
  :kube_controller_manager_args, :kube_cloud_controller_manager_args,
88
- :kubelet_args, :kube_proxy_args
89
+ :kubelet_args, :kube_proxy_args, :api_networks
89
90
 
90
91
  def find_worker_node_pools(configuration)
91
92
  configuration.fetch('worker_node_pools', [])
92
93
  end
93
94
 
94
95
  def latest_k3s_version
95
- response = HTTP.get('https://api.github.com/repos/k3s-io/k3s/tags').body
96
+ response = HTTParty.get('https://api.github.com/repos/k3s-io/k3s/tags').body
96
97
  JSON.parse(response).first['name']
97
98
  end
98
99
 
@@ -102,22 +103,22 @@ class Cluster
102
103
  end
103
104
 
104
105
  def delete_placement_groups
105
- Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:).delete
106
+ Hetzner::PlacementGroup.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete
106
107
 
107
108
  worker_node_pools.each do |pool|
108
109
  pool_name = pool['name']
109
- Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:, pool_name:).delete
110
+ Hetzner::PlacementGroup.new(hetzner_client: hetzner_client, cluster_name: cluster_name, pool_name: pool_name).delete
110
111
  end
111
112
  end
112
113
 
113
114
  def delete_resources
114
- Hetzner::LoadBalancer.new(hetzner_client:, cluster_name:).delete(high_availability: (masters.size > 1))
115
+ Hetzner::LoadBalancer.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(high_availability: (masters.size > 1))
115
116
 
116
- Hetzner::Firewall.new(hetzner_client:, cluster_name:).delete(all_servers)
117
+ Hetzner::Firewall.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(all_servers)
117
118
 
118
- Hetzner::Network.new(hetzner_client:, cluster_name:).delete
119
+ Hetzner::Network.new(hetzner_client: hetzner_client, cluster_name: cluster_name, existing_network: existing_network).delete
119
120
 
120
- Hetzner::SSHKey.new(hetzner_client:, cluster_name:).delete(public_ssh_key_path:)
121
+ Hetzner::SSHKey.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(public_ssh_key_path: public_ssh_key_path)
121
122
 
122
123
  delete_placement_groups
123
124
  delete_servers
@@ -195,7 +196,21 @@ class Cluster
195
196
  def master_script(master)
196
197
  server = master == first_master ? ' --cluster-init ' : " --server https://#{api_server_ip}:6443 "
197
198
  flannel_interface = find_flannel_interface(master)
198
- flannel_wireguard = enable_encryption ? ' --flannel-backend=wireguard ' : ' '
199
+
200
+ available_k3s_releases = Hetzner::Configuration.available_releases
201
+ wireguard_native_min_version_index = available_k3s_releases.find_index('v1.23.6+k3s1')
202
+ selected_version_index = available_k3s_releases.find_index(k3s_version)
203
+
204
+ flannel_wireguard = if enable_encryption
205
+ if selected_version_index >= wireguard_native_min_version_index
206
+ ' --flannel-backend=wireguard-native '
207
+ else
208
+ ' --flannel-backend=wireguard '
209
+ end
210
+ else
211
+ ' '
212
+ end
213
+
199
214
  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}"
200
215
  taint = schedule_workloads_on_masters? ? ' ' : ' --node-taint CriticalAddonsOnly=true:NoExecute '
201
216
 
@@ -293,7 +308,7 @@ class Cluster
293
308
  namespace: 'kube-system'
294
309
  name: 'hcloud'
295
310
  stringData:
296
- network: "#{cluster_name}"
311
+ network: "#{existing_network || cluster_name}"
297
312
  token: "#{configuration.hetzner_token}"
298
313
  EOF
299
314
  BASH
@@ -313,7 +328,7 @@ class Cluster
313
328
  puts
314
329
  puts 'Deploying k3s System Upgrade Controller...'
315
330
 
316
- cmd = 'kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.1/system-upgrade-controller.yaml'
331
+ cmd = 'kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.9.1/system-upgrade-controller.yaml'
317
332
 
318
333
  run cmd, kubeconfig_path: kubeconfig_path
319
334
 
@@ -340,7 +355,7 @@ class Cluster
340
355
 
341
356
  run cmd, kubeconfig_path: kubeconfig_path
342
357
 
343
- cmd = 'kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml'
358
+ cmd = 'kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/master/deploy/kubernetes/hcloud-csi.yml'
344
359
 
345
360
  run cmd, kubeconfig_path: kubeconfig_path
346
361
 
@@ -408,7 +423,7 @@ class Cluster
408
423
 
409
424
  masters.each do |master|
410
425
  master_private_ip = master['private_net'][0]['ip']
411
- sans << " --tls-san=#{master_private_ip} "
426
+ sans += " --tls-san=#{master_private_ip} "
412
427
  end
413
428
 
414
429
  sans
@@ -458,7 +473,7 @@ class Cluster
458
473
 
459
474
  def placement_group_id(pool_name = nil)
460
475
  @placement_groups ||= {}
461
- @placement_groups[pool_name || '__masters__'] ||= Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:, pool_name:).create
476
+ @placement_groups[pool_name || '__masters__'] ||= Hetzner::PlacementGroup.new(hetzner_client: hetzner_client, cluster_name: cluster_name, pool_name: pool_name).create
462
477
  end
463
478
 
464
479
  def master_instance_type
@@ -470,15 +485,15 @@ class Cluster
470
485
  end
471
486
 
472
487
  def firewall_id
473
- @firewall_id ||= Hetzner::Firewall.new(hetzner_client:, cluster_name:).create(high_availability: (masters_count > 1), networks:)
488
+ @firewall_id ||= Hetzner::Firewall.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(high_availability: (masters_count > 1), ssh_networks: ssh_networks, api_networks: api_networks)
474
489
  end
475
490
 
476
491
  def network_id
477
- @network_id ||= Hetzner::Network.new(hetzner_client:, cluster_name:).create(location: masters_location)
492
+ @network_id ||= Hetzner::Network.new(hetzner_client: hetzner_client, cluster_name: cluster_name, existing_network: existing_network).create(location: masters_location)
478
493
  end
479
494
 
480
495
  def ssh_key_id
481
- @ssh_key_id ||= Hetzner::SSHKey.new(hetzner_client:, cluster_name:).create(public_ssh_key_path:)
496
+ @ssh_key_id ||= Hetzner::SSHKey.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(public_ssh_key_path: public_ssh_key_path)
482
497
  end
483
498
 
484
499
  def master_definitions_for_create
@@ -489,13 +504,13 @@ class Cluster
489
504
  instance_type: master_instance_type,
490
505
  instance_id: "master#{i + 1}",
491
506
  location: masters_location,
492
- placement_group_id:,
493
- firewall_id:,
494
- network_id:,
495
- ssh_key_id:,
496
- image:,
497
- additional_packages:,
498
- additional_post_create_commands:
507
+ placement_group_id: placement_group_id,
508
+ firewall_id: firewall_id,
509
+ network_id: network_id,
510
+ ssh_key_id: ssh_key_id,
511
+ image: image,
512
+ additional_packages: additional_packages,
513
+ additional_post_create_commands: additional_post_create_commands
499
514
  }
500
515
  end
501
516
 
@@ -529,12 +544,12 @@ class Cluster
529
544
  instance_id: "pool-#{worker_node_pool_name}-worker#{i + 1}",
530
545
  placement_group_id: placement_group_id(worker_node_pool_name),
531
546
  location: worker_location,
532
- firewall_id:,
533
- network_id:,
534
- ssh_key_id:,
535
- image:,
536
- additional_packages:,
537
- additional_post_create_commands:
547
+ firewall_id: firewall_id,
548
+ network_id: network_id,
549
+ ssh_key_id: ssh_key_id,
550
+ image: image,
551
+ additional_packages: additional_packages,
552
+ additional_post_create_commands: additional_post_create_commands
538
553
  }
539
554
  end
540
555
 
@@ -542,7 +557,7 @@ class Cluster
542
557
  end
543
558
 
544
559
  def create_load_balancer
545
- Hetzner::LoadBalancer.new(hetzner_client:, cluster_name:).create(location: masters_location, network_id:)
560
+ Hetzner::LoadBalancer.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(location: masters_location, network_id: network_id)
546
561
  end
547
562
 
548
563
  def server_configs
@@ -562,7 +577,7 @@ class Cluster
562
577
 
563
578
  threads = server_configs.map do |server_config|
564
579
  Thread.new do
565
- servers << Hetzner::Server.new(hetzner_client:, cluster_name:).create(**server_config)
580
+ servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(**server_config)
566
581
  end
567
582
  end
568
583
 
@@ -584,7 +599,7 @@ class Cluster
584
599
  def delete_servers
585
600
  threads = all_servers.map do |server|
586
601
  Thread.new do
587
- Hetzner::Server.new(hetzner_client:, cluster_name:).delete(server_name: server['name'])
602
+ Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(server_name: server['name'])
588
603
  end
589
604
  end
590
605
 
@@ -642,4 +657,8 @@ class Cluster
642
657
  def hetzner_client
643
658
  configuration.hetzner_client
644
659
  end
660
+
661
+ def existing_network
662
+ configuration['existing_network']
663
+ end
645
664
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Hetzner
4
4
  class Configuration
5
- GITHUB_DELIM_LINKS = ','.freeze
6
- GITHUB_LINK_REGEX = /<([^>]+)>; rel=\"([^\"]+)\"/
5
+ GITHUB_DELIM_LINKS = ','
6
+ GITHUB_LINK_REGEX = /<([^>]+)>; rel="([^"]+)"/
7
7
 
8
8
  attr_reader :hetzner_client
9
9
 
@@ -52,7 +52,7 @@ module Hetzner
52
52
  releases = page_releases
53
53
  link_header = response.headers['link']
54
54
 
55
- while !link_header.nil?
55
+ until link_header.nil?
56
56
  next_page_url = extract_next_github_page_url(link_header)
57
57
 
58
58
  break if next_page_url.nil?
@@ -67,10 +67,10 @@ module Hetzner
67
67
  releases.sort
68
68
  end
69
69
  rescue StandardError
70
- if defined?errors
71
- errors << 'Cannot fetch the releases with Hetzner API, please try again later'
70
+ if defined? errors
71
+ errors << 'Cannot fetch the releases with Github API, please try again later. This may be due to API rate limits.'
72
72
  else
73
- puts 'Cannot fetch the releases with Hetzner API, please try again later'
73
+ puts 'Cannot fetch the releases with Github API, please try again later. This may be due to API rate limits.'
74
74
  end
75
75
  end
76
76
 
@@ -92,21 +92,20 @@ module Hetzner
92
92
  configuration
93
93
  end
94
94
 
95
- private
96
-
97
- attr_reader :configuration, :errors, :options
95
+ private_class_method
98
96
 
99
97
  def self.fetch_releases(url)
100
- response = HTTP.get(url)
98
+ response = HTTParty.get(url)
101
99
  [response, JSON.parse(response.body).map { |hash| hash['name'] }]
102
100
  end
103
101
 
104
102
  def self.extract_next_github_page_url(link_header)
105
103
  link_header.split(GITHUB_DELIM_LINKS).each do |link|
106
104
  GITHUB_LINK_REGEX.match(link.strip) do |match|
107
- url_part, meta_part = match[1], match[2]
105
+ url_part = match[1]
106
+ meta_part = match[2]
108
107
  next if !url_part || !meta_part
109
- return url_part if meta_part == "next"
108
+ return url_part if meta_part == 'next'
110
109
  end
111
110
  end
112
111
 
@@ -115,17 +114,22 @@ module Hetzner
115
114
 
116
115
  def self.assign_url_part(meta_part, url_part)
117
116
  case meta_part
118
- when "next"
117
+ when 'next'
119
118
  url_part
120
119
  end
121
120
  end
122
121
 
122
+ private
123
+
124
+ attr_reader :configuration, :errors, :options
125
+
123
126
  def validate_create
124
127
  validate_public_ssh_key
125
128
  validate_private_ssh_key
126
129
  validate_ssh_allowed_networks
130
+ validate_api_allowed_networks
127
131
  validate_masters_location
128
- validate_k3s_version
132
+ # validate_k3s_version
129
133
  validate_masters
130
134
  validate_worker_node_pools
131
135
  validate_verify_host_key
@@ -137,11 +141,12 @@ module Hetzner
137
141
  validate_kube_cloud_controller_manager_args
138
142
  validate_kubelet_args
139
143
  validate_kube_proxy_args
144
+ validate_existing_network
140
145
  end
141
146
 
142
147
  def validate_upgrade
143
148
  validate_kubeconfig_path_must_exist
144
- validate_new_k3s_version
149
+ # validate_new_k3s_version
145
150
  end
146
151
 
147
152
  def validate_public_ssh_key
@@ -165,11 +170,11 @@ module Hetzner
165
170
  errors << 'Invalid Private SSH key path'
166
171
  end
167
172
 
168
- def validate_ssh_allowed_networks
169
- networks ||= configuration['ssh_allowed_networks']
173
+ def validate_networks(configuration_option, access_type)
174
+ networks ||= configuration[configuration_option]
170
175
 
171
176
  if networks.nil? || networks.empty?
172
- errors << 'At least one network/IP range must be specified for SSH access'
177
+ errors << "At least one network/IP range must be specified for #{access_type} access"
173
178
  return
174
179
  end
175
180
 
@@ -181,7 +186,7 @@ module Hetzner
181
186
 
182
187
  unless invalid_networks.empty?
183
188
  invalid_networks.each do |network|
184
- errors << "The network #{network} is an invalid range"
189
+ errors << "The #{access_type} network #{network} is an invalid range"
185
190
  end
186
191
  end
187
192
 
@@ -191,7 +196,7 @@ module Hetzner
191
196
 
192
197
  unless invalid_ranges.empty?
193
198
  invalid_ranges.each do |_network|
194
- errors << 'Please use the CIDR notation for the networks to avoid ambiguity'
199
+ errors << 'Please use the CIDR notation for the #{access_type} networks to avoid ambiguity'
195
200
  end
196
201
  end
197
202
 
@@ -199,13 +204,30 @@ module Hetzner
199
204
 
200
205
  current_ip = URI.open('http://whatismyip.akamai.com').read
201
206
 
202
- current_ip_networks = networks.detect do |network|
207
+ current_ip_network = networks.detect do |network|
203
208
  IPAddr.new(network).include?(current_ip)
204
209
  rescue StandardError
205
210
  false
206
211
  end
207
212
 
208
- errors << "Your current IP #{current_ip} is not included into any of the networks you've specified, so we won't be able to SSH into the nodes" unless current_ip_networks
213
+ unless current_ip_network
214
+ case access_type
215
+ when "SSH"
216
+ errors << "Your current IP #{current_ip} is not included into any of the #{access_type} networks you've specified, so we won't be able to SSH into the nodes "
217
+ when "API"
218
+ errors << "Your current IP #{current_ip} is not included into any of the #{access_type} networks you've specified, so we won't be able to connect to the Kubernetes API"
219
+ end
220
+ end
221
+ end
222
+
223
+
224
+ def validate_ssh_allowed_networks
225
+ return
226
+ validate_networks('ssh_allowed_networks', 'SSH')
227
+ end
228
+
229
+ def validate_api_allowed_networks
230
+ validate_networks('api_allowed_networks', 'API')
209
231
  end
210
232
 
211
233
  def validate_masters_location
@@ -379,7 +401,7 @@ module Hetzner
379
401
 
380
402
  begin
381
403
  token = hetzner_token
382
- @hetzner_client = Hetzner::Client.new(token:)
404
+ @hetzner_client = Hetzner::Client.new(token: token)
383
405
  response = hetzner_client.get('/locations')
384
406
  error_code = response.dig('error', 'code')
385
407
  @valid = error_code != 'unauthorized'
@@ -450,5 +472,15 @@ module Hetzner
450
472
  @errors << 'Cannot fetch server types with Hetzner API, please try again later'
451
473
  false
452
474
  end
475
+
476
+ def validate_existing_network
477
+ return unless configuration['existing_network']
478
+
479
+ existing_network = Hetzner::Network.new(hetzner_client: hetzner_client, cluster_name: configuration['cluster_name'], existing_network: configuration['existing_network']).get
480
+
481
+ return if existing_network
482
+
483
+ @errors << "You have specified that you want to use the existing network named '#{configuration['existing_network']} but this network doesn't exist"
484
+ end
453
485
  end
454
486
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Hetzner
4
4
  module K3s
5
- VERSION = '0.5.8'
5
+ VERSION = '0.6.0.pre2'
6
6
  end
7
7
  end
data/lib/hetzner/utils.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Net::SSH::Transport::Algorithms::ALGORITHMS.values.each { |algs| algs.reject! { |a| a =~ /^ecd(sa|h)-sha2/ } }
4
+ Net::SSH::KnownHosts::SUPPORTED_TYPE.reject! { |t| t =~ /^ecd(sa|h)-sha2/ }
5
+
6
+ require 'childprocess'
7
+
3
8
  module Utils
4
9
  CMD_FILE_PATH = '/tmp/cli.cmd'
5
10
 
@@ -19,8 +24,6 @@ module Utils
19
24
  end
20
25
 
21
26
  def run(command, kubeconfig_path:)
22
- env = ENV.to_hash.merge({ 'KUBECONFIG' => kubeconfig_path })
23
-
24
27
  write_file CMD_FILE_PATH, <<-CONTENT
25
28
  set -euo pipefail
26
29
  #{command}
@@ -29,20 +32,19 @@ module Utils
29
32
  FileUtils.chmod('+x', CMD_FILE_PATH)
30
33
 
31
34
  begin
32
- process = nil
35
+ process = ChildProcess.build('bash', '-c', CMD_FILE_PATH)
36
+ process.io.inherit!
37
+ process.environment['KUBECONFIG'] = kubeconfig_path
38
+ process.environment['HCLOUD_TOKEN'] = ENV.fetch('HCLOUD_TOKEN', '')
33
39
 
34
40
  at_exit do
35
- process&.send_signal('SIGTERM')
41
+ process.stop
36
42
  rescue Errno::ESRCH, Interrupt
37
43
  # ignore
38
44
  end
39
45
 
40
- Subprocess.check_call(['bash', '-c', CMD_FILE_PATH], env:) do |p|
41
- process = p
42
- end
43
- rescue Subprocess::NonZeroExit
44
- puts 'Command failed: non-zero exit code'
45
- exit 1
46
+ process.start
47
+ process.wait
46
48
  rescue Interrupt
47
49
  puts 'Command interrupted'
48
50
  exit 1
@@ -86,6 +88,13 @@ module Utils
86
88
  end
87
89
  end
88
90
  output.chop
91
+ # rescue StandardError => e
92
+ # p [e.class, e.message]
93
+ # retries += 1
94
+ # retry unless retries > 15 || e.message =~ /Bad file descriptor/
95
+ rescue Timeout::Error, IOError, Errno::EBADF
96
+ retries += 1
97
+ retry unless retries > 15
89
98
  rescue Net::SSH::Disconnect => e
90
99
  retries += 1
91
100
  retry unless retries > 15 || e.message =~ /Too many authentication failures/