hetzner-k3s 0.1.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 600b092e02f2bca4fd0be7e830a13913d2bbf82d2e3b98226ab52a2b5df4e859
4
- data.tar.gz: f595dda56d1ca9aeaa611a77d82c168a63d300a83c5f3e8edc44b3da5790d46a
3
+ metadata.gz: 109610e9f4d807bac091471880141069488865da1024cee8bb0479846c772165
4
+ data.tar.gz: d4f882d488ecc94d6f7234fc122c4dc6241dfc8ce946cbce1855619353296917
5
5
  SHA512:
6
- metadata.gz: 6024a0b99ecc6d97d56e50f39e9e89f9cd2d92db7261604162ec37628df6bd4b942f50be64c4f05d9a745e55a3e18e567a759f20626671e81746c4160a280a9a
7
- data.tar.gz: 17f2095befd0035555adc902dc52baed71f77bd82ef26ecb9bff3df5e7b5ec14068cdd16923fd6a2297e22badaab922b3fbc745939285e41fed8699922e26017
6
+ metadata.gz: 6cd9649eef2f75f616f9dc5bc2bb86d2b10e0e227137adb6683c502b0ed42a0888f59f361e088edece98eb91c0ee11c596c8734c00865081564cb1539cb537ea
7
+ data.tar.gz: e1569ef139160e15daa78566cb48e71bf99999d5e6f19a98f30fc73b984bc7fb5fe0a208a4594c7d8c4606ffd6ea509409cc31cd48818739be1ed83be914c9b0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hetzner-k3s (0.1.0)
4
+ hetzner-k3s (0.3.1)
5
5
  http
6
6
  k8s-ruby
7
7
  net-ssh
data/README.md CHANGED
@@ -44,6 +44,7 @@ cluster_name: test
44
44
  kubeconfig_path: "./kubeconfig"
45
45
  k3s_version: v1.21.3+k3s1
46
46
  ssh_key_path: "~/.ssh/id_rsa.pub"
47
+ verify_host_key: false
47
48
  location: nbg1
48
49
  masters:
49
50
  instance_type: cpx21
@@ -53,7 +54,7 @@ worker_node_pools:
53
54
  instance_type: cpx21
54
55
  instance_count: 4
55
56
  - name: big
56
- instance_type: cp321
57
+ instance_type: cpx31
57
58
  instance_count: 2
58
59
  ```
59
60
 
@@ -74,6 +75,8 @@ curl \
74
75
  ```
75
76
 
76
77
 
78
+ Note: the option `verify_host_key` is by default set to `false` to disable host key verification. This is because sometimes when creating new servers, Hetzner may assign IP addresses that were previously used by other servers you owned in the past. Therefore the host key verification would fail. If you set this option to `true` and this happens, the tool won't be able to continue creating the cluster until you resolve the issue with one of the suggestions it will give you.
79
+
77
80
  Finally, to create the cluster run:
78
81
 
79
82
  ```bash
@@ -208,6 +211,23 @@ The other annotations should be self explanatory. You can find a list of the ava
208
211
 
209
212
  Once the cluster is ready you can create persistent volumes out of the box with the default storage class `hcloud-volumes`, since the Hetzner CSI driver is installed automatically. This will use Hetzner's block storage (based on Ceph so it's replicated and highly available) for your persistent volumes. Note that the minimum size of a volume is 10Gi. If you specify a smaller size for a volume, the volume will be created with a capacity of 10Gi anyway.
210
213
 
214
+
215
+ ## changelog
216
+
217
+ - 0.3.2
218
+ - Configure DNS to use Cloudflare's resolver instead of Hetzner's, since Hetzner's resolvers are not always reliable
219
+
220
+ - 0.3.1
221
+ - Allow enabling/disabling the host key verification
222
+
223
+ - 0.3.0
224
+ - Handle case when an SSH key with the given fingerprint already exists in the Hetzner project
225
+ - Handle a timeout of 5 seconds for requests to the Hetzner API
226
+ - Retry waiting for server to be up when timeouts/host-unreachable errors occur
227
+ - Ignore known_hosts entry to prevent errors when recreating servers with IPs that have been used previously
228
+
229
+ - 0.2.0
230
+ - Allow mixing servers of different series Intel/AMD
211
231
  ## Contributing and support
212
232
 
213
233
  Please create a PR if you want to propose any changes, or open an issue if you are having trouble with the tool - I will do my best to help if I can.
@@ -218,4 +238,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
218
238
 
219
239
  ## Code of Conduct
220
240
 
221
- Everyone interacting in the hetzner-k3s project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/vitobotta/k3s/blob/master/CODE_OF_CONDUCT.md).
241
+ Everyone interacting in the hetzner-k3s project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/vitobotta/hetzner-k3s/blob/main/CODE_OF_CONDUCT.md).
@@ -4,6 +4,7 @@ cluster_name: test
4
4
  kubeconfig_path: "../kubeconfig"
5
5
  k3s_version: v1.21.3+k3s1
6
6
  ssh_key_path: "~/.ssh/id_rsa.pub"
7
+ verify_host_key: false
7
8
  location: nbg1
8
9
  masters:
9
10
  instance_type: cpx21
@@ -9,15 +9,21 @@ module Hetzner
9
9
  end
10
10
 
11
11
  def get(path)
12
- JSON.parse HTTP.headers(headers).get(BASE_URI + path).body
12
+ make_request do
13
+ JSON.parse HTTP.headers(headers).get(BASE_URI + path).body
14
+ end
13
15
  end
14
16
 
15
17
  def post(path, data)
16
- HTTP.headers(headers).post(BASE_URI + path, json: data)
18
+ make_request do
19
+ HTTP.headers(headers).post(BASE_URI + path, json: data)
20
+ end
17
21
  end
18
22
 
19
23
  def delete(path, id)
20
- HTTP.headers(headers).delete(BASE_URI + path + "/" + id.to_s)
24
+ make_request do
25
+ HTTP.headers(headers).delete(BASE_URI + path + "/" + id.to_s)
26
+ end
21
27
  end
22
28
 
23
29
  private
@@ -28,5 +34,13 @@ module Hetzner
28
34
  "Content-Type": "application/json"
29
35
  }
30
36
  end
37
+
38
+ def make_request &block
39
+ Timeout::timeout(5) do
40
+ block.call
41
+ end
42
+ rescue Timeout::Error
43
+ retry
44
+ end
31
45
  end
32
46
  end
@@ -26,12 +26,12 @@ module Hetzner
26
26
  JSON.parse(response)["load_balancer"]["id"]
27
27
  end
28
28
 
29
- def delete
29
+ def delete(ha:)
30
30
  if load_balancer = find_load_balancer
31
- puts "Deleting API load balancer..."
31
+ puts "Deleting API load balancer..." unless ha
32
32
  hetzner_client.delete("/load_balancers", load_balancer["id"])
33
- puts "...API load balancer deleted."
34
- else
33
+ puts "...API load balancer deleted." unless ha
34
+ elsif ha
35
35
  puts "API load balancer no longer exists, skipping."
36
36
  end
37
37
 
@@ -74,7 +74,12 @@ module Hetzner
74
74
  - sed -i 's/[#]*PermitRootLogin yes/PermitRootLogin prohibit-password/g' /etc/ssh/sshd_config
75
75
  - sed -i 's/[#]*PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config
76
76
  - systemctl restart sshd
77
- EOS
77
+ - systemctl stop systemd-resolved
78
+ - systemctl disable systemd-resolved
79
+ - rm /etc/resolv.conf
80
+ - echo "nameserver 1.1.1.1" > /etc/resolv.conf
81
+ - echo "nameserver 1.0.0.1" >> /etc/resolv.conf
82
+ EOS
78
83
  end
79
84
 
80
85
  end
@@ -26,11 +26,17 @@ module Hetzner
26
26
  JSON.parse(response)["ssh_key"]["id"]
27
27
  end
28
28
 
29
- def delete
29
+ def delete(ssh_key_path:)
30
+ @ssh_key_path = ssh_key_path
31
+
30
32
  if ssh_key = find_ssh_key
31
- puts "Deleting ssh_key..."
32
- hetzner_client.delete("/ssh_keys", ssh_key["id"])
33
- puts "...ssh_key deleted."
33
+ if ssh_key["name"] == cluster_name
34
+ puts "Deleting ssh_key..."
35
+ hetzner_client.delete("/ssh_keys", ssh_key["id"])
36
+ puts "...ssh_key deleted."
37
+ else
38
+ puts "The SSH key existed before creating the cluster, so I won't delete it."
39
+ end
34
40
  else
35
41
  puts "SSH key no longer exists, skipping."
36
42
  end
@@ -42,15 +48,33 @@ module Hetzner
42
48
 
43
49
  attr_reader :hetzner_client, :cluster_name, :ssh_key_path
44
50
 
51
+ def public_key
52
+ @public_key ||= File.read(ssh_key_path).chop
53
+ end
54
+
45
55
  def ssh_key_config
46
56
  {
47
57
  name: cluster_name,
48
- public_key: File.read(ssh_key_path)
58
+ public_key: public_key
49
59
  }
50
60
  end
51
61
 
62
+ def fingerprint
63
+ @fingerprint ||= ::SSHKey.fingerprint(public_key)
64
+ end
65
+
52
66
  def find_ssh_key
53
- hetzner_client.get("/ssh_keys")["ssh_keys"].detect{ |ssh_key| ssh_key["name"] == cluster_name }
67
+ key = hetzner_client.get("/ssh_keys")["ssh_keys"].detect do |ssh_key|
68
+ ssh_key["fingerprint"] == fingerprint
69
+ end
70
+
71
+ unless key
72
+ key = hetzner_client.get("/ssh_keys")["ssh_keys"].detect do |ssh_key|
73
+ ssh_key["name"] == cluster_name
74
+ end
75
+ end
76
+
77
+ key
54
78
  end
55
79
 
56
80
  end
@@ -80,7 +80,7 @@ module Hetzner
80
80
  validate_k3s_version
81
81
  validate_masters
82
82
  validate_worker_node_pools
83
- validate_all_nodes_must_be_of_same_series
83
+ validate_verify_host_key
84
84
  when :delete
85
85
  validate_kubeconfig_path_must_exist
86
86
  when :upgrade
@@ -221,11 +221,6 @@ module Hetzner
221
221
  end
222
222
  end
223
223
 
224
- def validate_all_nodes_must_be_of_same_series
225
- series = used_server_types.map{ |used_server_type| used_server_type[0..1]}
226
- errors << "Master and worker node pools must all be of the same server series for networking to function properly (available series: cx, cp, ccx)" unless series.uniq.size == 1
227
- end
228
-
229
224
  def validate_new_k3s_version_must_be_more_recent
230
225
  return if options[:force] == "true"
231
226
  return unless kubernetes_client
@@ -298,6 +293,12 @@ module Hetzner
298
293
  errors << "Cannot connect to the Kubernetes cluster"
299
294
  false
300
295
  end
296
+
297
+
298
+ def validate_verify_host_key
299
+ return unless [true, false].include?(configuration.fetch("ssh_key_path", false))
300
+ errors << "Please set the verify_host_key option to either true or false"
301
+ end
301
302
  end
302
303
  end
303
304
  end
@@ -29,7 +29,7 @@ class Cluster
29
29
  @masters_config = configuration.dig("masters")
30
30
  @worker_node_pools = configuration.dig("worker_node_pools")
31
31
  @location = configuration.dig("location")
32
- @flannel_interface = find_flannel_interface(configuration.dig("masters")["instance_type"])
32
+ @verify_host_key = configuration.fetch("verify_host_key", false)
33
33
  @servers = []
34
34
 
35
35
  create_resources
@@ -46,6 +46,7 @@ class Cluster
46
46
  def delete(configuration:)
47
47
  @cluster_name = configuration.dig("cluster_name")
48
48
  @kubeconfig_path = File.expand_path(configuration.dig("kubeconfig_path"))
49
+ @ssh_key_path = File.expand_path(configuration.dig("ssh_key_path"))
49
50
 
50
51
  delete_resources
51
52
  end
@@ -66,9 +67,9 @@ class Cluster
66
67
 
67
68
  attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
68
69
  :masters_config, :worker_node_pools,
69
- :location, :flannel_interface, :ssh_key_path, :kubernetes_client,
70
+ :location, :ssh_key_path, :kubernetes_client,
70
71
  :hetzner_token, :tls_sans, :new_k3s_version, :configuration,
71
- :config_file
72
+ :config_file, :verify_host_key
72
73
 
73
74
 
74
75
  def latest_k3s_version
@@ -138,17 +139,18 @@ class Cluster
138
139
  end
139
140
  end
140
141
 
141
- threads.each(&:join)
142
+ threads.each(&:join) unless threads.empty?
142
143
 
143
144
  puts
144
145
  threads = servers.map do |server|
145
146
  Thread.new { wait_for_ssh server }
146
147
  end
147
148
 
148
- threads.each(&:join)
149
+ threads.each(&:join) unless threads.empty?
149
150
  end
150
151
 
151
152
  def delete_resources
153
+ # Deleting nodes defined according to Kubernetes first
152
154
  begin
153
155
  Timeout::timeout(5) do
154
156
  servers = kubernetes_client.api("v1").resource("nodes").list
@@ -159,12 +161,23 @@ class Cluster
159
161
  end
160
162
  end
161
163
 
162
- threads.each(&:join)
164
+ threads.each(&:join) unless threads.empty?
163
165
  end
164
- rescue Timeout::Error
166
+ rescue Timeout::Error, Excon::Error::Socket
165
167
  puts "Unable to fetch nodes from Kubernetes API. Is the cluster online?"
166
168
  end
167
169
 
170
+ # Deleting nodes defined in the config file just in case there are leftovers i.e. nodes that
171
+ # were not part of the cluster for some reason
172
+
173
+ threads = all_servers.map do |server|
174
+ Thread.new do
175
+ Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(server_name: server["name"])
176
+ end
177
+ end
178
+
179
+ threads.each(&:join) unless threads.empty?
180
+
168
181
  puts
169
182
 
170
183
  sleep 5 # give time for the servers to actually be deleted
@@ -182,12 +195,12 @@ class Cluster
182
195
  Hetzner::SSHKey.new(
183
196
  hetzner_client: hetzner_client,
184
197
  cluster_name: cluster_name
185
- ).delete
198
+ ).delete(ssh_key_path: ssh_key_path)
186
199
 
187
200
  Hetzner::LoadBalancer.new(
188
201
  hetzner_client: hetzner_client,
189
202
  cluster_name: cluster_name
190
- ).delete
203
+ ).delete(ha: (masters.size > 1))
191
204
 
192
205
  end
193
206
 
@@ -216,6 +229,7 @@ class Cluster
216
229
 
217
230
  def master_script(master)
218
231
  server = master == first_master ? " --cluster-init " : " --server https://#{first_master_private_ip}:6443 "
232
+ flannel_interface = find_flannel_interface(master)
219
233
 
220
234
  <<~EOF
221
235
  curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
@@ -242,7 +256,9 @@ class Cluster
242
256
  EOF
243
257
  end
244
258
 
245
- def worker_script
259
+ def worker_script(worker)
260
+ flannel_interface = find_flannel_interface(worker)
261
+
246
262
  <<~EOF
247
263
  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 \
248
264
  --node-name="$(hostname -f)" \
@@ -277,7 +293,7 @@ class Cluster
277
293
  end
278
294
  end
279
295
 
280
- threads.each(&:join)
296
+ threads.each(&:join) unless threads.empty?
281
297
  end
282
298
 
283
299
  threads = workers.map do |worker|
@@ -285,14 +301,14 @@ class Cluster
285
301
  puts
286
302
  puts "Deploying k3s to worker (#{worker["name"]})..."
287
303
 
288
- ssh worker, worker_script, print_output: true
304
+ ssh worker, worker_script(worker), print_output: true
289
305
 
290
306
  puts
291
307
  puts "...k3s has been deployed to worker (#{worker["name"]})."
292
308
  end
293
309
  end
294
310
 
295
- threads.each(&:join)
311
+ threads.each(&:join) unless threads.empty?
296
312
  end
297
313
 
298
314
  def deploy_cloud_controller_manager
@@ -429,17 +445,19 @@ class Cluster
429
445
  end
430
446
 
431
447
  def wait_for_ssh(server)
432
- server_name = server["name"]
448
+ Timeout::timeout(5) do
449
+ server_name = server["name"]
433
450
 
434
- puts "Waiting for server #{server_name} to be up..."
451
+ puts "Waiting for server #{server_name} to be up..."
435
452
 
436
- loop do
437
- result = ssh(server, "echo UP")
438
- break if result == "UP"
439
- end
453
+ loop do
454
+ result = ssh(server, "echo UP")
455
+ break if result == "UP"
456
+ end
440
457
 
441
- puts "...server #{server_name} is now up."
442
- rescue Errno::ENETUNREACH
458
+ puts "...server #{server_name} is now up."
459
+ end
460
+ rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error
443
461
  retry
444
462
  end
445
463
 
@@ -447,20 +465,23 @@ class Cluster
447
465
  public_ip = server.dig("public_net", "ipv4", "ip")
448
466
  output = ""
449
467
 
450
- Net::SSH.start(public_ip, "root") do |session|
468
+ Net::SSH.start(public_ip, "root", verify_host_key: (verify_host_key ? :always : :never)) do |session|
451
469
  session.exec!(command) do |channel, stream, data|
452
470
  output << data
453
471
  puts data if print_output
454
472
  end
455
473
  end
456
-
457
474
  output.chop
458
- rescue Net::SSH::ConnectionTimeout
459
- retry
460
475
  rescue Net::SSH::Disconnect => e
461
476
  retry unless e.message =~ /Too many authentication failures/
462
- rescue Errno::ECONNREFUSED
477
+ rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
463
478
  retry
479
+ rescue Net::SSH::HostKeyMismatch
480
+ puts
481
+ puts "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."
482
+ puts "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."
483
+ puts "If are sure no security is being violated here and you're just creating new servers, you can eiher remove the relevant lines from your known_hosts (see IPs from the cloud console) or disable host key verification by setting the option 'verify_host_key' to false in the configuration file for the cluster."
484
+ exit 1
464
485
  end
465
486
 
466
487
  def kubernetes_client
@@ -471,14 +492,11 @@ class Cluster
471
492
  @kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
472
493
  end
473
494
 
474
- def find_flannel_interface(server_type)
475
- case server_type[0..1]
476
- when "cp"
477
- "enp7s0"
478
- when "cc"
479
- "enp7s0"
480
- when "cx"
495
+ def find_flannel_interface(server)
496
+ if ssh(server, "lscpu | grep Vendor") =~ /Intel/
481
497
  "ens10"
498
+ else
499
+ "enp7s0"
482
500
  end
483
501
  end
484
502
 
@@ -1,5 +1,5 @@
1
1
  module Hetzner
2
2
  module K3s
3
- VERSION = "0.1.0"
3
+ VERSION = "0.3.2"
4
4
  end
5
5
  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.1.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vito Botta
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-07 00:00:00.000000000 Z
11
+ date: 2021-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor