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 +4 -4
- data/Gemfile.lock +5 -1
- data/README.md +24 -1
- data/cluster_config.yaml.example +1 -0
- data/hetzner-k3s.gemspec +2 -0
- data/lib/hetzner/infra/client.rb +17 -3
- data/lib/hetzner/infra/load_balancer.rb +4 -4
- data/lib/hetzner/infra/server.rb +6 -1
- data/lib/hetzner/infra/ssh_key.rb +30 -6
- data/lib/hetzner/k3s/cli.rb +7 -0
- data/lib/hetzner/k3s/cluster.rb +41 -22
- data/lib/hetzner/k3s/version.rb +1 -1
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f45064341000a4cf3a3ec6aef60c2b26eb95758e3c0e1d16c87740b8e549834
|
4
|
+
data.tar.gz: 0ec683973e5c27811c8f9a2d82804ce3721d9a93137c46d15087f8e06a9fe4b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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/
|
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).
|
data/cluster_config.yaml.example
CHANGED
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.
|
data/lib/hetzner/infra/client.rb
CHANGED
@@ -9,15 +9,21 @@ module Hetzner
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def get(path)
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
data/lib/hetzner/infra/server.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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:
|
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
|
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
|
data/lib/hetzner/k3s/cli.rb
CHANGED
@@ -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
|
data/lib/hetzner/k3s/cluster.rb
CHANGED
@@ -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
|
-
|
448
|
+
Timeout::timeout(5) do
|
449
|
+
server_name = server["name"]
|
435
450
|
|
436
|
-
|
451
|
+
puts "Waiting for server #{server_name} to be up..."
|
437
452
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
453
|
+
loop do
|
454
|
+
result = ssh(server, "echo UP")
|
455
|
+
break if result == "UP"
|
456
|
+
end
|
442
457
|
|
443
|
-
|
444
|
-
|
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
|
data/lib/hetzner/k3s/version.rb
CHANGED
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.
|
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-
|
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:
|