hetzner-k3s 0.2.0 → 0.3.3

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: a6c87ae355b79344110f8b822668072438b9b496b602177b3d6528e56a75aea8
4
- data.tar.gz: 9be4a8945d32c22f4810389b9330707177749d8c18d6416d73a6bbbdef4ff3a9
3
+ metadata.gz: 4f45064341000a4cf3a3ec6aef60c2b26eb95758e3c0e1d16c87740b8e549834
4
+ data.tar.gz: 0ec683973e5c27811c8f9a2d82804ce3721d9a93137c46d15087f8e06a9fe4b2
5
5
  SHA512:
6
- metadata.gz: 7c852a9f0b9b4e3dd80f5c33007f39d4191a9588874cac0d03904c15f185686757e45793525a0bf2b87787fe4d8de3969318797300e40aad4811cb74c883628f
7
- data.tar.gz: 321d20a09b451ef355095268cf0da68ebefd88a43c3b7261293923b346b5ee5641ee55214cda1a8b53aaf44fa8fedb178303b1e9dd9d7de5856332c135546628
6
+ metadata.gz: 7ac95d48d017c7997c3e20b7dd3d5160bf5188a4bff9144efc382606bf2d83462221287b8ba9c16ff62b5f3687486f310f042add4c50f06228e0f2da4afb63db
7
+ data.tar.gz: bd2caff255115cdd82582b5c143249ca92983d4232ab77ba03fe5d4719fc812dfb3af7b8617fc6adaa0c9093ef66c0b6f1cdbdee5f60c25e5f2f819fc6fa8f0b
data/Gemfile.lock CHANGED
@@ -1,7 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hetzner-k3s (0.1.0)
4
+ hetzner-k3s (0.3.3)
5
+ bcrypt_pbkdf
6
+ ed25519
5
7
  http
6
8
  k8s-ruby
7
9
  net-ssh
@@ -13,6 +15,7 @@ GEM
13
15
  specs:
14
16
  addressable (2.8.0)
15
17
  public_suffix (>= 2.0.2, < 5.0)
18
+ bcrypt_pbkdf (1.1.0)
16
19
  concurrent-ruby (1.1.9)
17
20
  diff-lcs (1.4.4)
18
21
  domain_name (0.5.20190701)
@@ -43,6 +46,7 @@ GEM
43
46
  dry-equalizer (~> 0.2)
44
47
  dry-inflector (~> 0.1, >= 0.1.2)
45
48
  dry-logic (~> 0.4, >= 0.4.2)
49
+ ed25519 (1.2.4)
46
50
  excon (0.85.0)
47
51
  ffi (1.15.3)
48
52
  ffi-compiler (1.0.1)
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
@@ -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,26 @@ 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.3
218
+ - Add some gems required on Linux
219
+
220
+ - 0.3.2
221
+ - Configure DNS to use Cloudflare's resolver instead of Hetzner's, since Hetzner's resolvers are not always reliable
222
+
223
+ - 0.3.1
224
+ - Allow enabling/disabling the host key verification
225
+
226
+ - 0.3.0
227
+ - Handle case when an SSH key with the given fingerprint already exists in the Hetzner project
228
+ - Handle a timeout of 5 seconds for requests to the Hetzner API
229
+ - Retry waiting for server to be up when timeouts/host-unreachable errors occur
230
+ - Ignore known_hosts entry to prevent errors when recreating servers with IPs that have been used previously
231
+
232
+ - 0.2.0
233
+ - Allow mixing servers of different series Intel/AMD
211
234
  ## Contributing and support
212
235
 
213
236
  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 +241,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
218
241
 
219
242
  ## Code of Conduct
220
243
 
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).
244
+ 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
data/hetzner-k3s.gemspec CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency "net-ssh"
24
24
  spec.add_dependency "k8s-ruby"
25
25
  spec.add_dependency "sshkey"
26
+ spec.add_dependency "ed25519"
27
+ spec.add_dependency "bcrypt_pbkdf"
26
28
 
27
29
  # Specify which files should be added to the gem when it is released.
28
30
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -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,6 +80,7 @@ module Hetzner
80
80
  validate_k3s_version
81
81
  validate_masters
82
82
  validate_worker_node_pools
83
+ validate_verify_host_key
83
84
  when :delete
84
85
  validate_kubeconfig_path_must_exist
85
86
  when :upgrade
@@ -292,6 +293,12 @@ module Hetzner
292
293
  errors << "Cannot connect to the Kubernetes cluster"
293
294
  false
294
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
295
302
  end
296
303
  end
297
304
  end
@@ -29,6 +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
+ @verify_host_key = configuration.fetch("verify_host_key", false)
32
33
  @servers = []
33
34
 
34
35
  create_resources
@@ -45,6 +46,7 @@ class Cluster
45
46
  def delete(configuration:)
46
47
  @cluster_name = configuration.dig("cluster_name")
47
48
  @kubeconfig_path = File.expand_path(configuration.dig("kubeconfig_path"))
49
+ @ssh_key_path = File.expand_path(configuration.dig("ssh_key_path"))
48
50
 
49
51
  delete_resources
50
52
  end
@@ -67,7 +69,7 @@ class Cluster
67
69
  :masters_config, :worker_node_pools,
68
70
  :location, :ssh_key_path, :kubernetes_client,
69
71
  :hetzner_token, :tls_sans, :new_k3s_version, :configuration,
70
- :config_file
72
+ :config_file, :verify_host_key
71
73
 
72
74
 
73
75
  def latest_k3s_version
@@ -137,17 +139,18 @@ class Cluster
137
139
  end
138
140
  end
139
141
 
140
- threads.each(&:join)
142
+ threads.each(&:join) unless threads.empty?
141
143
 
142
144
  puts
143
145
  threads = servers.map do |server|
144
146
  Thread.new { wait_for_ssh server }
145
147
  end
146
148
 
147
- threads.each(&:join)
149
+ threads.each(&:join) unless threads.empty?
148
150
  end
149
151
 
150
152
  def delete_resources
153
+ # Deleting nodes defined according to Kubernetes first
151
154
  begin
152
155
  Timeout::timeout(5) do
153
156
  servers = kubernetes_client.api("v1").resource("nodes").list
@@ -158,12 +161,23 @@ class Cluster
158
161
  end
159
162
  end
160
163
 
161
- threads.each(&:join)
164
+ threads.each(&:join) unless threads.empty?
162
165
  end
163
- rescue Timeout::Error
166
+ rescue Timeout::Error, Excon::Error::Socket
164
167
  puts "Unable to fetch nodes from Kubernetes API. Is the cluster online?"
165
168
  end
166
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
+
167
181
  puts
168
182
 
169
183
  sleep 5 # give time for the servers to actually be deleted
@@ -181,12 +195,12 @@ class Cluster
181
195
  Hetzner::SSHKey.new(
182
196
  hetzner_client: hetzner_client,
183
197
  cluster_name: cluster_name
184
- ).delete
198
+ ).delete(ssh_key_path: ssh_key_path)
185
199
 
186
200
  Hetzner::LoadBalancer.new(
187
201
  hetzner_client: hetzner_client,
188
202
  cluster_name: cluster_name
189
- ).delete
203
+ ).delete(ha: (masters.size > 1))
190
204
 
191
205
  end
192
206
 
@@ -279,7 +293,7 @@ class Cluster
279
293
  end
280
294
  end
281
295
 
282
- threads.each(&:join)
296
+ threads.each(&:join) unless threads.empty?
283
297
  end
284
298
 
285
299
  threads = workers.map do |worker|
@@ -294,7 +308,7 @@ class Cluster
294
308
  end
295
309
  end
296
310
 
297
- threads.each(&:join)
311
+ threads.each(&:join) unless threads.empty?
298
312
  end
299
313
 
300
314
  def deploy_cloud_controller_manager
@@ -431,17 +445,19 @@ class Cluster
431
445
  end
432
446
 
433
447
  def wait_for_ssh(server)
434
- server_name = server["name"]
448
+ Timeout::timeout(5) do
449
+ server_name = server["name"]
435
450
 
436
- puts "Waiting for server #{server_name} to be up..."
451
+ puts "Waiting for server #{server_name} to be up..."
437
452
 
438
- loop do
439
- result = ssh(server, "echo UP")
440
- break if result == "UP"
441
- end
453
+ loop do
454
+ result = ssh(server, "echo UP")
455
+ break if result == "UP"
456
+ end
442
457
 
443
- puts "...server #{server_name} is now up."
444
- rescue Errno::ENETUNREACH
458
+ puts "...server #{server_name} is now up."
459
+ end
460
+ rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error
445
461
  retry
446
462
  end
447
463
 
@@ -449,20 +465,23 @@ class Cluster
449
465
  public_ip = server.dig("public_net", "ipv4", "ip")
450
466
  output = ""
451
467
 
452
- 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|
453
469
  session.exec!(command) do |channel, stream, data|
454
470
  output << data
455
471
  puts data if print_output
456
472
  end
457
473
  end
458
-
459
474
  output.chop
460
- rescue Net::SSH::ConnectionTimeout
461
- retry
462
475
  rescue Net::SSH::Disconnect => e
463
476
  retry unless e.message =~ /Too many authentication failures/
464
- rescue Errno::ECONNREFUSED
477
+ rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
465
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
466
485
  end
467
486
 
468
487
  def kubernetes_client
@@ -1,5 +1,5 @@
1
1
  module Hetzner
2
2
  module K3s
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.3"
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.2.0
4
+ version: 0.3.3
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-08 00:00:00.000000000 Z
11
+ date: 2021-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -80,6 +80,34 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ed25519
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bcrypt_pbkdf
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  description: A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using
84
112
  k3s.
85
113
  email: