hetzner-k3s 0.4.6 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +6 -52
- data/README.md +37 -4
- data/bin/build.sh +3 -3
- data/cluster_config.yaml.example +8 -4
- data/hetzner-k3s.gemspec +1 -1
- data/lib/hetzner/infra/client.rb +4 -2
- data/lib/hetzner/infra/server.rb +12 -7
- data/lib/hetzner/k3s/cli.rb +16 -48
- data/lib/hetzner/k3s/cluster.rb +136 -253
- data/lib/hetzner/k3s/version.rb +1 -1
- data/lib/hetzner/utils.rb +99 -0
- metadata +12 -11
- data/lib/hetzner/k3s/client_patch.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21205ebaea746e26fa42437afe5feca2c9fb856861e6eebedd0608b023d0addf
|
4
|
+
data.tar.gz: e67f315b4b96e98fab4fc514e1a01ab88e79b701c83177e0c53863150e508d10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2bf2e628f2326e63c10ebe076882979913f260a80c8b01b54944ef226341bf5cbe091283b168af566e6eda13ad84ad9742832284b28973ceb96ea87d75c911b
|
7
|
+
data.tar.gz: d5c21906a5eb59772d7613a821bb76a451e95e6f3b4f5b39321c1c431c27de5451c6fc890e37e8622077aab8c54669132227fbfd4622c85cc9838c5bd616c2b1
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.1.0
|
data/Dockerfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
hetzner-k3s (0.4.
|
4
|
+
hetzner-k3s (0.4.8)
|
5
5
|
bcrypt_pbkdf
|
6
6
|
ed25519
|
7
7
|
http
|
8
|
-
k8s-ruby
|
9
8
|
net-ssh
|
10
9
|
sshkey
|
10
|
+
subprocess
|
11
11
|
thor
|
12
12
|
|
13
13
|
GEM
|
@@ -16,43 +16,14 @@ GEM
|
|
16
16
|
addressable (2.8.0)
|
17
17
|
public_suffix (>= 2.0.2, < 5.0)
|
18
18
|
bcrypt_pbkdf (1.1.0)
|
19
|
-
concurrent-ruby (1.1.9)
|
20
19
|
diff-lcs (1.4.4)
|
21
20
|
domain_name (0.5.20190701)
|
22
21
|
unf (>= 0.0.5, < 1.0.0)
|
23
|
-
dry-configurable (0.13.0)
|
24
|
-
concurrent-ruby (~> 1.0)
|
25
|
-
dry-core (~> 0.6)
|
26
|
-
dry-container (0.9.0)
|
27
|
-
concurrent-ruby (~> 1.0)
|
28
|
-
dry-configurable (~> 0.13, >= 0.13.0)
|
29
|
-
dry-core (0.7.1)
|
30
|
-
concurrent-ruby (~> 1.0)
|
31
|
-
dry-equalizer (0.3.0)
|
32
|
-
dry-inflector (0.2.1)
|
33
|
-
dry-logic (0.6.1)
|
34
|
-
concurrent-ruby (~> 1.0)
|
35
|
-
dry-core (~> 0.2)
|
36
|
-
dry-equalizer (~> 0.2)
|
37
|
-
dry-struct (0.5.1)
|
38
|
-
dry-core (~> 0.4, >= 0.4.3)
|
39
|
-
dry-equalizer (~> 0.2)
|
40
|
-
dry-types (~> 0.13)
|
41
|
-
ice_nine (~> 0.11)
|
42
|
-
dry-types (0.13.4)
|
43
|
-
concurrent-ruby (~> 1.0)
|
44
|
-
dry-container (~> 0.3)
|
45
|
-
dry-core (~> 0.4, >= 0.4.4)
|
46
|
-
dry-equalizer (~> 0.2)
|
47
|
-
dry-inflector (~> 0.1, >= 0.1.2)
|
48
|
-
dry-logic (~> 0.4, >= 0.4.2)
|
49
22
|
ed25519 (1.2.4)
|
50
|
-
|
51
|
-
ffi (1.15.3)
|
23
|
+
ffi (1.15.4)
|
52
24
|
ffi-compiler (1.0.1)
|
53
25
|
ffi (>= 1.0.0)
|
54
26
|
rake
|
55
|
-
hashdiff (1.0.1)
|
56
27
|
http (4.4.1)
|
57
28
|
addressable (~> 2.3)
|
58
29
|
http-cookie (~> 1.0)
|
@@ -63,24 +34,9 @@ GEM
|
|
63
34
|
http-form_data (2.3.0)
|
64
35
|
http-parser (1.2.3)
|
65
36
|
ffi-compiler (>= 1.0, < 2.0)
|
66
|
-
ice_nine (0.11.2)
|
67
|
-
jsonpath (0.9.9)
|
68
|
-
multi_json
|
69
|
-
to_regexp (~> 0.2.1)
|
70
|
-
k8s-ruby (0.10.5)
|
71
|
-
dry-struct (~> 0.5.0)
|
72
|
-
dry-types (~> 0.13.0)
|
73
|
-
excon (~> 0.71)
|
74
|
-
hashdiff (~> 1.0.0)
|
75
|
-
jsonpath (~> 0.9.5)
|
76
|
-
recursive-open-struct (~> 1.1.0)
|
77
|
-
yajl-ruby (~> 1.4.0)
|
78
|
-
yaml-safe_load_stream (~> 0.1)
|
79
|
-
multi_json (1.15.0)
|
80
37
|
net-ssh (6.1.0)
|
81
38
|
public_suffix (4.0.6)
|
82
39
|
rake (12.3.3)
|
83
|
-
recursive-open-struct (1.1.3)
|
84
40
|
rspec (3.10.0)
|
85
41
|
rspec-core (~> 3.10.0)
|
86
42
|
rspec-expectations (~> 3.10.0)
|
@@ -95,13 +51,11 @@ GEM
|
|
95
51
|
rspec-support (~> 3.10.0)
|
96
52
|
rspec-support (3.10.2)
|
97
53
|
sshkey (2.0.0)
|
98
|
-
|
99
|
-
|
54
|
+
subprocess (1.5.5)
|
55
|
+
thor (1.2.1)
|
100
56
|
unf (0.1.4)
|
101
57
|
unf_ext
|
102
58
|
unf_ext (0.0.8)
|
103
|
-
yajl-ruby (1.4.1)
|
104
|
-
yaml-safe_load_stream (0.1.1)
|
105
59
|
|
106
60
|
PLATFORMS
|
107
61
|
ruby
|
@@ -112,4 +66,4 @@ DEPENDENCIES
|
|
112
66
|
rspec (~> 3.0)
|
113
67
|
|
114
68
|
BUNDLED WITH
|
115
|
-
2.
|
69
|
+
2.3.4
|
data/README.md
CHANGED
@@ -38,7 +38,7 @@ This will install the `hetzner-k3s` executable in your PATH.
|
|
38
38
|
Alternatively, if you don't want to set up a Ruby runtime but have Docker installed, you can use a container. Run the following from inside the directory where you have the config file for the cluster (described in the next section):
|
39
39
|
|
40
40
|
```bash
|
41
|
-
docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.
|
41
|
+
docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.5.0 create-cluster --config-file /cluster/test.yaml
|
42
42
|
```
|
43
43
|
|
44
44
|
Replace `test.yaml` with the name of your config file.
|
@@ -70,6 +70,9 @@ worker_node_pools:
|
|
70
70
|
- name: big
|
71
71
|
instance_type: cpx31
|
72
72
|
instance_count: 2
|
73
|
+
additional_packages:
|
74
|
+
- somepackage
|
75
|
+
enable_ipsec_encryption: true
|
73
76
|
```
|
74
77
|
|
75
78
|
It should hopefully be self explanatory; you can run `hetzner-k3s releases` to see a list of the available releases from the most recent to the oldest available.
|
@@ -97,8 +100,20 @@ curl \
|
|
97
100
|
'https://api.hetzner.cloud/v1/server_types'
|
98
101
|
```
|
99
102
|
|
103
|
+
By default, the image in use is Ubuntu 20.04, but you can specify an image to use with the `image` config option. This makes it also possible
|
104
|
+
to use a snapshot that you have already created from and existing server (for example to preinstall some tools). If you want to use a custom
|
105
|
+
snapshot you'll need to specify the **ID** of the snapshot/image, not the description you gave when you created the template server. To find
|
106
|
+
the ID of your custom image/snapshot, run:
|
100
107
|
|
101
|
-
|
108
|
+
```bash
|
109
|
+
curl \
|
110
|
+
-H "Authorization: Bearer $API_TOKEN" \
|
111
|
+
'https://api.hetzner.cloud/v1/images'
|
112
|
+
```
|
113
|
+
|
114
|
+
Note that if you use a custom image, the creation of the servers may take longer than when using the default image.
|
115
|
+
|
116
|
+
Also 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.
|
102
117
|
|
103
118
|
Finally, to create the cluster run:
|
104
119
|
|
@@ -199,7 +214,7 @@ kubectl label node <master1> <master2> <master2> plan.upgrade.cattle.io/k3s-serv
|
|
199
214
|
To delete a cluster, running
|
200
215
|
|
201
216
|
```bash
|
202
|
-
hetzner-k3s delete-cluster --config-file cluster_config.
|
217
|
+
hetzner-k3s delete-cluster --config-file cluster_config.yaml
|
203
218
|
```
|
204
219
|
|
205
220
|
This will delete all the resources in the Hetzner Cloud project for the cluster being deleted.
|
@@ -242,7 +257,25 @@ I recommend that you create a separate Hetzner project for each cluster, because
|
|
242
257
|
|
243
258
|
## changelog
|
244
259
|
|
245
|
-
- 0.
|
260
|
+
- 0.5.0
|
261
|
+
- Allow installing additional packages when creating the servers
|
262
|
+
- Allow enabling ipsec encryption
|
263
|
+
|
264
|
+
- 0.4.9
|
265
|
+
- Ensure the program always exits with exit code 1 if the config file fails validation
|
266
|
+
- Upgrade System Upgrade Controller to 0.8.1
|
267
|
+
- Remove dependency on unmaintained gem k8s-ruby
|
268
|
+
- Make the gem compatible with Ruby 3.1.0
|
269
|
+
|
270
|
+
- 0.4.8
|
271
|
+
- Increase timeout with API requests to 30 seconds
|
272
|
+
- Limit number of retries for API requests to 3
|
273
|
+
- Ensure all version tags are listed for k3s (thanks @janosmiko)
|
274
|
+
|
275
|
+
- 0.4.7
|
276
|
+
- Made it possible to specify a custom image/snapshot for the servers
|
277
|
+
|
278
|
+
- 0.4.6
|
246
279
|
- Added a check to abort gracefully when for some reason one or more servers are not created, for example due to temporary problems with the Hetzner API.
|
247
280
|
|
248
281
|
- 0.4.5
|
data/bin/build.sh
CHANGED
@@ -6,9 +6,9 @@ set -e
|
|
6
6
|
|
7
7
|
IMAGE="vitobotta/hetzner-k3s"
|
8
8
|
|
9
|
-
docker build -t ${IMAGE}:v0.
|
9
|
+
docker build -t ${IMAGE}:v0.5.0 \
|
10
10
|
--platform=linux/amd64 \
|
11
|
-
--cache-from ${IMAGE}:v0.4.
|
11
|
+
--cache-from ${IMAGE}:v0.4.9 \
|
12
12
|
--build-arg BUILDKIT_INLINE_CACHE=1 .
|
13
13
|
|
14
|
-
docker push vitobotta/hetzner-k3s:v0.
|
14
|
+
docker push vitobotta/hetzner-k3s:v0.5.0
|
data/cluster_config.yaml.example
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
---
|
2
|
-
hetzner_token:
|
2
|
+
hetzner_token: <your token>
|
3
3
|
cluster_name: test
|
4
|
-
kubeconfig_path: "
|
4
|
+
kubeconfig_path: "./kubeconfig"
|
5
5
|
k3s_version: v1.21.3+k3s1
|
6
|
-
|
6
|
+
public_ssh_key_path: "~/.ssh/id_rsa.pub"
|
7
|
+
private_ssh_key_path: "~/.ssh/id_rsa"
|
8
|
+
ssh_allowed_networks:
|
9
|
+
- 0.0.0.0/0
|
7
10
|
verify_host_key: false
|
8
11
|
location: nbg1
|
12
|
+
schedule_workloads_on_masters: false
|
9
13
|
masters:
|
10
14
|
instance_type: cpx21
|
11
15
|
instance_count: 3
|
@@ -14,5 +18,5 @@ worker_node_pools:
|
|
14
18
|
instance_type: cpx21
|
15
19
|
instance_count: 4
|
16
20
|
- name: big
|
17
|
-
instance_type:
|
21
|
+
instance_type: cpx31
|
18
22
|
instance_count: 2
|
data/hetzner-k3s.gemspec
CHANGED
@@ -21,10 +21,10 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_dependency "thor"
|
22
22
|
spec.add_dependency "http"
|
23
23
|
spec.add_dependency "net-ssh"
|
24
|
-
spec.add_dependency "k8s-ruby"
|
25
24
|
spec.add_dependency "sshkey"
|
26
25
|
spec.add_dependency "ed25519"
|
27
26
|
spec.add_dependency "bcrypt_pbkdf"
|
27
|
+
spec.add_dependency "subprocess"
|
28
28
|
|
29
29
|
# Specify which files should be added to the gem when it is released.
|
30
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
data/lib/hetzner/infra/server.rb
CHANGED
@@ -5,7 +5,9 @@ module Hetzner
|
|
5
5
|
@cluster_name = cluster_name
|
6
6
|
end
|
7
7
|
|
8
|
-
def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:, placement_group_id:)
|
8
|
+
def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:, placement_group_id:, image:, additional_packages: [])
|
9
|
+
@additional_packages = additional_packages
|
10
|
+
|
9
11
|
puts
|
10
12
|
|
11
13
|
server_name = "#{cluster_name}-#{instance_type}-#{instance_id}"
|
@@ -21,7 +23,7 @@ module Hetzner
|
|
21
23
|
server_config = {
|
22
24
|
name: server_name,
|
23
25
|
location: location,
|
24
|
-
image:
|
26
|
+
image: image,
|
25
27
|
firewalls: [
|
26
28
|
{ firewall: firewall_id }
|
27
29
|
],
|
@@ -70,17 +72,20 @@ module Hetzner
|
|
70
72
|
|
71
73
|
private
|
72
74
|
|
73
|
-
attr_reader :hetzner_client, :cluster_name
|
75
|
+
attr_reader :hetzner_client, :cluster_name, :additional_packages
|
74
76
|
|
75
77
|
def find_server(server_name)
|
76
|
-
hetzner_client.get("/servers")["servers"].detect{ |network| network["name"] == server_name }
|
78
|
+
hetzner_client.get("/servers?sort=created:desc")["servers"].detect{ |network| network["name"] == server_name }
|
77
79
|
end
|
78
80
|
|
79
81
|
def user_data
|
82
|
+
packages = ["fail2ban"]
|
83
|
+
packages += additional_packages if additional_packages
|
84
|
+
packages = "'" + packages.join("', '") + "'"
|
85
|
+
|
80
86
|
<<~EOS
|
81
87
|
#cloud-config
|
82
|
-
packages:
|
83
|
-
- fail2ban
|
88
|
+
packages: [#{packages}]
|
84
89
|
runcmd:
|
85
90
|
- sed -i 's/[#]*PermitRootLogin yes/PermitRootLogin prohibit-password/g' /etc/ssh/sshd_config
|
86
91
|
- sed -i 's/[#]*PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config
|
@@ -90,7 +95,7 @@ module Hetzner
|
|
90
95
|
- rm /etc/resolv.conf
|
91
96
|
- echo "nameserver 1.1.1.1" > /etc/resolv.conf
|
92
97
|
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
93
|
-
|
98
|
+
EOS
|
94
99
|
end
|
95
100
|
|
96
101
|
end
|
data/lib/hetzner/k3s/cli.rb
CHANGED
@@ -3,10 +3,12 @@ require "http"
|
|
3
3
|
require "sshkey"
|
4
4
|
require 'ipaddr'
|
5
5
|
require 'open-uri'
|
6
|
+
require "yaml"
|
6
7
|
|
7
8
|
require_relative "cluster"
|
8
9
|
require_relative "version"
|
9
10
|
|
11
|
+
|
10
12
|
module Hetzner
|
11
13
|
module K3s
|
12
14
|
class CLI < Thor
|
@@ -24,7 +26,6 @@ module Hetzner
|
|
24
26
|
|
25
27
|
def create_cluster
|
26
28
|
validate_config_file :create
|
27
|
-
|
28
29
|
Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).create configuration: configuration
|
29
30
|
end
|
30
31
|
|
@@ -64,14 +65,17 @@ module Hetzner
|
|
64
65
|
if File.exists?(config_file_path)
|
65
66
|
begin
|
66
67
|
@configuration = YAML.load_file(options[:config_file])
|
67
|
-
|
68
|
-
|
68
|
+
unless configuration.is_a? Hash
|
69
|
+
raise "Configuration is invalid"
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
rescue => e
|
69
73
|
puts "Please ensure that the config file is a correct YAML manifest."
|
70
|
-
|
74
|
+
exit 1
|
71
75
|
end
|
72
76
|
else
|
73
77
|
puts "Please specify a correct path for the config file."
|
74
|
-
|
78
|
+
exit 1
|
75
79
|
end
|
76
80
|
|
77
81
|
@errors = []
|
@@ -91,12 +95,12 @@ module Hetzner
|
|
91
95
|
validate_masters
|
92
96
|
validate_worker_node_pools
|
93
97
|
validate_verify_host_key
|
98
|
+
validate_additional_packages
|
94
99
|
when :delete
|
95
100
|
validate_kubeconfig_path_must_exist
|
96
101
|
when :upgrade
|
97
102
|
validate_kubeconfig_path_must_exist
|
98
103
|
validate_new_k3s_version
|
99
|
-
validate_new_k3s_version_must_be_more_recent
|
100
104
|
end
|
101
105
|
|
102
106
|
errors.flatten!
|
@@ -198,7 +202,7 @@ module Hetzner
|
|
198
202
|
|
199
203
|
def find_available_releases
|
200
204
|
@available_releases ||= begin
|
201
|
-
response = HTTP.get("https://api.github.com/repos/k3s-io/k3s/tags").body
|
205
|
+
response = HTTP.get("https://api.github.com/repos/k3s-io/k3s/tags?per_page=999").body
|
202
206
|
JSON.parse(response).map { |hash| hash["name"] }
|
203
207
|
end
|
204
208
|
rescue
|
@@ -271,36 +275,6 @@ module Hetzner
|
|
271
275
|
schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
|
272
276
|
end
|
273
277
|
|
274
|
-
def validate_new_k3s_version_must_be_more_recent
|
275
|
-
return if options[:force] == "true"
|
276
|
-
return unless kubernetes_client
|
277
|
-
|
278
|
-
begin
|
279
|
-
Timeout::timeout(5) do
|
280
|
-
servers = kubernetes_client.api("v1").resource("nodes").list
|
281
|
-
|
282
|
-
if servers.size == 0
|
283
|
-
errors << "The cluster seems to have no nodes, nothing to upgrade"
|
284
|
-
else
|
285
|
-
available_releases = find_available_releases
|
286
|
-
|
287
|
-
current_k3s_version = servers.first.dig(:status, :nodeInfo, :kubeletVersion)
|
288
|
-
current_k3s_version_index = available_releases.index(current_k3s_version) || 1000
|
289
|
-
|
290
|
-
new_k3s_version = options[:new_k3s_version]
|
291
|
-
new_k3s_version_index = available_releases.index(new_k3s_version) || 1000
|
292
|
-
|
293
|
-
unless new_k3s_version_index < current_k3s_version_index
|
294
|
-
errors << "The new k3s version must be more recent than the current one"
|
295
|
-
end
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
rescue Timeout::Error
|
300
|
-
puts "Cannot upgrade: Unable to fetch nodes from Kubernetes API. Is the cluster online?"
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
278
|
def validate_instance_group(instance_group, workers: true)
|
305
279
|
instance_group_errors = []
|
306
280
|
|
@@ -333,17 +307,6 @@ module Hetzner
|
|
333
307
|
errors << instance_group_errors
|
334
308
|
end
|
335
309
|
|
336
|
-
def kubernetes_client
|
337
|
-
return @kubernetes_client if @kubernetes_client
|
338
|
-
|
339
|
-
config_hash = YAML.load_file(File.expand_path(configuration["kubeconfig_path"]))
|
340
|
-
config_hash['current-context'] = configuration["cluster_name"]
|
341
|
-
@kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
|
342
|
-
rescue
|
343
|
-
errors << "Cannot connect to the Kubernetes cluster"
|
344
|
-
false
|
345
|
-
end
|
346
|
-
|
347
310
|
def validate_verify_host_key
|
348
311
|
return unless [true, false].include?(configuration.fetch("public_ssh_key_path", false))
|
349
312
|
errors << "Please set the verify_host_key option to either true or false"
|
@@ -396,6 +359,11 @@ module Hetzner
|
|
396
359
|
end
|
397
360
|
end
|
398
361
|
|
362
|
+
def validate_additional_packages
|
363
|
+
additional_packages = configuration.dig("additional_packages")
|
364
|
+
errors << "Invalid additional packages configuration - it should be an array" if additional_packages && !additional_packages.is_a?(Array)
|
365
|
+
end
|
366
|
+
|
399
367
|
end
|
400
368
|
end
|
401
369
|
end
|
data/lib/hetzner/k3s/cluster.rb
CHANGED
@@ -2,8 +2,8 @@ require 'thread'
|
|
2
2
|
require 'net/ssh'
|
3
3
|
require "securerandom"
|
4
4
|
require "base64"
|
5
|
-
require "k8s-ruby"
|
6
5
|
require 'timeout'
|
6
|
+
require "subprocess"
|
7
7
|
|
8
8
|
require_relative "../infra/client"
|
9
9
|
require_relative "../infra/firewall"
|
@@ -13,10 +13,12 @@ require_relative "../infra/server"
|
|
13
13
|
require_relative "../infra/load_balancer"
|
14
14
|
require_relative "../infra/placement_group"
|
15
15
|
|
16
|
-
require_relative "../
|
16
|
+
require_relative "../utils"
|
17
17
|
|
18
18
|
|
19
19
|
class Cluster
|
20
|
+
include Utils
|
21
|
+
|
20
22
|
def initialize(hetzner_client:, hetzner_token:)
|
21
23
|
@hetzner_client = hetzner_client
|
22
24
|
@hetzner_token = hetzner_token
|
@@ -36,6 +38,7 @@ class Cluster
|
|
36
38
|
@verify_host_key = configuration.fetch("verify_host_key", false)
|
37
39
|
@servers = []
|
38
40
|
@networks = configuration.dig("ssh_allowed_networks")
|
41
|
+
@enable_ipsec_encryption = configuration.fetch("enable_ipsec_encryption", false)
|
39
42
|
|
40
43
|
create_resources
|
41
44
|
|
@@ -76,9 +79,10 @@ class Cluster
|
|
76
79
|
|
77
80
|
attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
|
78
81
|
:masters_config, :worker_node_pools,
|
79
|
-
:location, :public_ssh_key_path,
|
82
|
+
:location, :public_ssh_key_path,
|
80
83
|
:hetzner_token, :tls_sans, :new_k3s_version, :configuration,
|
81
|
-
:config_file, :verify_host_key, :networks, :private_ssh_key_path,
|
84
|
+
:config_file, :verify_host_key, :networks, :private_ssh_key_path,
|
85
|
+
:configuration, :enable_ipsec_encryption
|
82
86
|
|
83
87
|
|
84
88
|
def latest_k3s_version
|
@@ -120,7 +124,9 @@ class Cluster
|
|
120
124
|
firewall_id: firewall_id,
|
121
125
|
network_id: network_id,
|
122
126
|
ssh_key_id: ssh_key_id,
|
123
|
-
placement_group_id: placement_group_id
|
127
|
+
placement_group_id: placement_group_id,
|
128
|
+
image: image,
|
129
|
+
additional_packages: additional_packages,
|
124
130
|
}
|
125
131
|
end
|
126
132
|
|
@@ -144,22 +150,23 @@ class Cluster
|
|
144
150
|
firewall_id: firewall_id,
|
145
151
|
network_id: network_id,
|
146
152
|
ssh_key_id: ssh_key_id,
|
147
|
-
placement_group_id: placement_group_id
|
153
|
+
placement_group_id: placement_group_id,
|
154
|
+
image: image,
|
155
|
+
additional_packages: additional_packages,
|
148
156
|
}
|
149
157
|
end
|
150
158
|
end
|
151
159
|
|
152
160
|
threads = server_configs.map do |server_config|
|
153
161
|
Thread.new do
|
154
|
-
servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(server_config)
|
162
|
+
servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(**server_config)
|
155
163
|
end
|
156
164
|
end
|
157
165
|
|
158
166
|
threads.each(&:join) unless threads.empty?
|
159
167
|
|
160
|
-
|
161
|
-
|
162
|
-
exit 1
|
168
|
+
while servers.size != server_configs.size
|
169
|
+
sleep 1
|
163
170
|
end
|
164
171
|
|
165
172
|
puts
|
@@ -206,31 +213,78 @@ class Cluster
|
|
206
213
|
end
|
207
214
|
|
208
215
|
def upgrade_cluster
|
209
|
-
|
210
|
-
|
211
|
-
begin
|
212
|
-
kubernetes_client.api("upgrade.cattle.io/v1").resource("plans").get("k3s-server", namespace: "system-upgrade")
|
213
|
-
|
214
|
-
puts "Aborting - an upgrade is already in progress."
|
215
|
-
|
216
|
-
rescue K8s::Error::NotFound
|
217
|
-
resources.each do |resource|
|
218
|
-
kubernetes_client.create_resource(resource)
|
219
|
-
end
|
220
|
-
|
221
|
-
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."
|
222
|
-
puts "The API server may be briefly unavailable during the upgrade of the controlplane."
|
223
|
-
|
224
|
-
configuration["k3s_version"] = new_k3s_version
|
216
|
+
worker_upgrade_concurrency = workers.size - 1
|
217
|
+
worker_upgrade_concurrency = 1 if worker_upgrade_concurrency == 0
|
225
218
|
|
226
|
-
|
227
|
-
|
219
|
+
cmd = <<~EOS
|
220
|
+
kubectl apply -f - <<-EOF
|
221
|
+
apiVersion: upgrade.cattle.io/v1
|
222
|
+
kind: Plan
|
223
|
+
metadata:
|
224
|
+
name: k3s-server
|
225
|
+
namespace: system-upgrade
|
226
|
+
labels:
|
227
|
+
k3s-upgrade: server
|
228
|
+
spec:
|
229
|
+
concurrency: 1
|
230
|
+
version: #{new_k3s_version}
|
231
|
+
nodeSelector:
|
232
|
+
matchExpressions:
|
233
|
+
- {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
|
234
|
+
serviceAccountName: system-upgrade
|
235
|
+
tolerations:
|
236
|
+
- key: "CriticalAddonsOnly"
|
237
|
+
operator: "Equal"
|
238
|
+
value: "true"
|
239
|
+
effect: "NoExecute"
|
240
|
+
cordon: true
|
241
|
+
upgrade:
|
242
|
+
image: rancher/k3s-upgrade
|
243
|
+
EOF
|
244
|
+
EOS
|
245
|
+
|
246
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
247
|
+
|
248
|
+
cmd = <<~EOS
|
249
|
+
kubectl apply -f - <<-EOF
|
250
|
+
apiVersion: upgrade.cattle.io/v1
|
251
|
+
kind: Plan
|
252
|
+
metadata:
|
253
|
+
name: k3s-agent
|
254
|
+
namespace: system-upgrade
|
255
|
+
labels:
|
256
|
+
k3s-upgrade: agent
|
257
|
+
spec:
|
258
|
+
concurrency: #{worker_upgrade_concurrency}
|
259
|
+
version: #{new_k3s_version}
|
260
|
+
nodeSelector:
|
261
|
+
matchExpressions:
|
262
|
+
- {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
|
263
|
+
serviceAccountName: system-upgrade
|
264
|
+
prepare:
|
265
|
+
image: rancher/k3s-upgrade
|
266
|
+
args: ["prepare", "k3s-server"]
|
267
|
+
cordon: true
|
268
|
+
upgrade:
|
269
|
+
image: rancher/k3s-upgrade
|
270
|
+
EOF
|
271
|
+
EOS
|
272
|
+
|
273
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
274
|
+
|
275
|
+
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."
|
276
|
+
puts "The API server may be briefly unavailable during the upgrade of the controlplane."
|
277
|
+
|
278
|
+
configuration["k3s_version"] = new_k3s_version
|
279
|
+
|
280
|
+
File.write(config_file, configuration.to_yaml)
|
228
281
|
end
|
229
282
|
|
230
283
|
|
231
284
|
def master_script(master)
|
232
285
|
server = master == first_master ? " --cluster-init " : " --server https://#{first_master_private_ip}:6443 "
|
233
286
|
flannel_interface = find_flannel_interface(master)
|
287
|
+
flannel_ipsec = enable_ipsec_encryption ? " --flannel-backend=ipsec " : " "
|
234
288
|
|
235
289
|
taint = schedule_workloads_on_masters? ? " " : " --node-taint CriticalAddonsOnly=true:NoExecute "
|
236
290
|
|
@@ -245,6 +299,7 @@ class Cluster
|
|
245
299
|
--node-name="$(hostname -f)" \
|
246
300
|
--cluster-cidr=10.244.0.0/16 \
|
247
301
|
--etcd-expose-metrics=true \
|
302
|
+
#{flannel_ipsec} \
|
248
303
|
--kube-controller-manager-arg="address=0.0.0.0" \
|
249
304
|
--kube-controller-manager-arg="bind-address=0.0.0.0" \
|
250
305
|
--kube-proxy-arg="metrics-bind-address=0.0.0.0" \
|
@@ -316,201 +371,71 @@ class Cluster
|
|
316
371
|
end
|
317
372
|
|
318
373
|
def deploy_cloud_controller_manager
|
374
|
+
check_kubectl
|
375
|
+
|
319
376
|
puts
|
320
377
|
puts "Deploying Hetzner Cloud Controller Manager..."
|
321
378
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
network: Base64.encode64(cluster_name),
|
335
|
-
token: Base64.encode64(hetzner_token)
|
336
|
-
}
|
337
|
-
)
|
338
|
-
|
339
|
-
kubernetes_client.api('v1').resource('secrets').create_resource(secret)
|
340
|
-
end
|
341
|
-
|
342
|
-
|
343
|
-
manifest = fetch_manifest("https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml")
|
379
|
+
cmd = <<~EOS
|
380
|
+
kubectl apply -f - <<-EOF
|
381
|
+
apiVersion: "v1"
|
382
|
+
kind: "Secret"
|
383
|
+
metadata:
|
384
|
+
namespace: 'kube-system'
|
385
|
+
name: 'hcloud'
|
386
|
+
stringData:
|
387
|
+
network: "#{cluster_name}"
|
388
|
+
token: "#{hetzner_token}"
|
389
|
+
EOF
|
390
|
+
EOS
|
344
391
|
|
345
|
-
|
392
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
346
393
|
|
347
|
-
|
394
|
+
cmd = "kubectl apply -f https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml"
|
348
395
|
|
349
|
-
|
350
|
-
kubernetes_client.api("apps/v1").resource("deployments").get("hcloud-cloud-controller-manager", namespace: "kube-system")
|
351
|
-
|
352
|
-
resources.each do |resource|
|
353
|
-
kubernetes_client.update_resource(resource)
|
354
|
-
end
|
355
|
-
|
356
|
-
rescue K8s::Error::NotFound
|
357
|
-
resources.each do |resource|
|
358
|
-
kubernetes_client.create_resource(resource)
|
359
|
-
end
|
360
|
-
|
361
|
-
end
|
396
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
362
397
|
|
363
398
|
puts "...Cloud Controller Manager deployed"
|
364
|
-
rescue Excon::Error::Socket
|
365
|
-
retry
|
366
|
-
end
|
367
|
-
|
368
|
-
def fetch_manifest(url)
|
369
|
-
retries ||= 1
|
370
|
-
HTTP.follow.get(url).body
|
371
|
-
rescue
|
372
|
-
retry if (retries += 1) <= 10
|
373
399
|
end
|
374
400
|
|
375
401
|
def deploy_system_upgrade_controller
|
402
|
+
check_kubectl
|
403
|
+
|
376
404
|
puts
|
377
405
|
puts "Deploying k3s System Upgrade Controller..."
|
378
406
|
|
379
|
-
|
380
|
-
|
381
|
-
File.write("/tmp/system-upgrade-controller.yaml", manifest)
|
382
|
-
|
383
|
-
resources = K8s::Resource.from_files("/tmp/system-upgrade-controller.yaml")
|
407
|
+
cmd = "kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.1/system-upgrade-controller.yaml"
|
384
408
|
|
385
|
-
|
386
|
-
kubernetes_client.api("apps/v1").resource("deployments").get("system-upgrade-controller", namespace: "system-upgrade")
|
387
|
-
|
388
|
-
resources.each do |resource|
|
389
|
-
kubernetes_client.update_resource(resource)
|
390
|
-
end
|
391
|
-
|
392
|
-
rescue K8s::Error::NotFound
|
393
|
-
resources.each do |resource|
|
394
|
-
kubernetes_client.create_resource(resource)
|
395
|
-
end
|
396
|
-
|
397
|
-
end
|
409
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
398
410
|
|
399
411
|
puts "...k3s System Upgrade Controller deployed"
|
400
|
-
rescue Excon::Error::Socket
|
401
|
-
retry
|
402
412
|
end
|
403
413
|
|
404
414
|
def deploy_csi_driver
|
415
|
+
check_kubectl
|
416
|
+
|
405
417
|
puts
|
406
418
|
puts "Deploying Hetzner CSI Driver..."
|
407
419
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
data: {
|
420
|
-
token: Base64.encode64(hetzner_token)
|
421
|
-
}
|
422
|
-
)
|
423
|
-
|
424
|
-
kubernetes_client.api('v1').resource('secrets').create_resource(secret)
|
425
|
-
end
|
426
|
-
|
427
|
-
|
428
|
-
manifest = HTTP.follow.get("https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml").body
|
429
|
-
|
430
|
-
File.write("/tmp/csi-driver.yaml", manifest)
|
420
|
+
cmd = <<~EOS
|
421
|
+
kubectl apply -f - <<-EOF
|
422
|
+
apiVersion: "v1"
|
423
|
+
kind: "Secret"
|
424
|
+
metadata:
|
425
|
+
namespace: 'kube-system'
|
426
|
+
name: 'hcloud-csi'
|
427
|
+
stringData:
|
428
|
+
token: "#{hetzner_token}"
|
429
|
+
EOF
|
430
|
+
EOS
|
431
431
|
|
432
|
-
|
432
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
433
433
|
|
434
|
-
|
435
|
-
kubernetes_client.api("apps/v1").resource("daemonsets").get("hcloud-csi-node", namespace: "kube-system")
|
434
|
+
cmd = "kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml"
|
436
435
|
|
437
|
-
|
438
|
-
resources.each do |resource|
|
439
|
-
begin
|
440
|
-
kubernetes_client.update_resource(resource)
|
441
|
-
rescue K8s::Error::Invalid => e
|
442
|
-
raise e unless e.message =~ /must be specified/i
|
443
|
-
end
|
444
|
-
end
|
445
|
-
|
446
|
-
rescue K8s::Error::NotFound
|
447
|
-
resources.each do |resource|
|
448
|
-
kubernetes_client.create_resource(resource)
|
449
|
-
end
|
450
|
-
|
451
|
-
end
|
436
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
452
437
|
|
453
438
|
puts "...CSI Driver deployed"
|
454
|
-
rescue Excon::Error::Socket
|
455
|
-
retry
|
456
|
-
end
|
457
|
-
|
458
|
-
def wait_for_ssh(server)
|
459
|
-
Timeout::timeout(5) do
|
460
|
-
server_name = server["name"]
|
461
|
-
|
462
|
-
puts "Waiting for server #{server_name} to be up..."
|
463
|
-
|
464
|
-
loop do
|
465
|
-
result = ssh(server, "echo UP")
|
466
|
-
break if result == "UP"
|
467
|
-
end
|
468
|
-
|
469
|
-
puts "...server #{server_name} is now up."
|
470
|
-
end
|
471
|
-
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error, IOError
|
472
|
-
retry
|
473
|
-
end
|
474
|
-
|
475
|
-
def ssh(server, command, print_output: false)
|
476
|
-
public_ip = server.dig("public_net", "ipv4", "ip")
|
477
|
-
output = ""
|
478
|
-
|
479
|
-
params = { verify_host_key: (verify_host_key ? :always : :never) }
|
480
|
-
|
481
|
-
if private_ssh_key_path
|
482
|
-
params[:keys] = [private_ssh_key_path]
|
483
|
-
end
|
484
|
-
|
485
|
-
Net::SSH.start(public_ip, "root", params) do |session|
|
486
|
-
session.exec!(command) do |channel, stream, data|
|
487
|
-
output << data
|
488
|
-
puts data if print_output
|
489
|
-
end
|
490
|
-
end
|
491
|
-
output.chop
|
492
|
-
rescue Net::SSH::Disconnect => e
|
493
|
-
retry unless e.message =~ /Too many authentication failures/
|
494
|
-
rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
|
495
|
-
retry
|
496
|
-
rescue Net::SSH::AuthenticationFailed
|
497
|
-
puts
|
498
|
-
puts "Cannot continue: SSH authentication failed. Please ensure that the private SSH key is correct."
|
499
|
-
exit 1
|
500
|
-
rescue Net::SSH::HostKeyMismatch
|
501
|
-
puts
|
502
|
-
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."
|
503
|
-
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."
|
504
|
-
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."
|
505
|
-
exit 1
|
506
|
-
end
|
507
|
-
|
508
|
-
def kubernetes_client
|
509
|
-
return @kubernetes_client if @kubernetes_client
|
510
|
-
|
511
|
-
config_hash = YAML.load_file(kubeconfig_path)
|
512
|
-
config_hash['current-context'] = cluster_name
|
513
|
-
@kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
|
514
439
|
end
|
515
440
|
|
516
441
|
def find_flannel_interface(server)
|
@@ -522,7 +447,7 @@ class Cluster
|
|
522
447
|
end
|
523
448
|
|
524
449
|
def all_servers
|
525
|
-
@all_servers ||= hetzner_client.get("/servers")["servers"].select{ |server| belongs_to_cluster?(server) == true }
|
450
|
+
@all_servers ||= hetzner_client.get("/servers?sort=created:desc")["servers"].select{ |server| belongs_to_cluster?(server) == true }
|
526
451
|
end
|
527
452
|
|
528
453
|
def masters
|
@@ -590,63 +515,6 @@ class Cluster
|
|
590
515
|
FileUtils.chmod "go-r", kubeconfig_path
|
591
516
|
end
|
592
517
|
|
593
|
-
def ugrade_plan_manifest_path
|
594
|
-
worker_upgrade_concurrency = workers.size - 1
|
595
|
-
worker_upgrade_concurrency = 1 if worker_upgrade_concurrency == 0
|
596
|
-
|
597
|
-
manifest = <<~EOF
|
598
|
-
apiVersion: upgrade.cattle.io/v1
|
599
|
-
kind: Plan
|
600
|
-
metadata:
|
601
|
-
name: k3s-server
|
602
|
-
namespace: system-upgrade
|
603
|
-
labels:
|
604
|
-
k3s-upgrade: server
|
605
|
-
spec:
|
606
|
-
concurrency: 1
|
607
|
-
version: #{new_k3s_version}
|
608
|
-
nodeSelector:
|
609
|
-
matchExpressions:
|
610
|
-
- {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
|
611
|
-
serviceAccountName: system-upgrade
|
612
|
-
tolerations:
|
613
|
-
- key: "CriticalAddonsOnly"
|
614
|
-
operator: "Equal"
|
615
|
-
value: "true"
|
616
|
-
effect: "NoExecute"
|
617
|
-
cordon: true
|
618
|
-
upgrade:
|
619
|
-
image: rancher/k3s-upgrade
|
620
|
-
---
|
621
|
-
apiVersion: upgrade.cattle.io/v1
|
622
|
-
kind: Plan
|
623
|
-
metadata:
|
624
|
-
name: k3s-agent
|
625
|
-
namespace: system-upgrade
|
626
|
-
labels:
|
627
|
-
k3s-upgrade: agent
|
628
|
-
spec:
|
629
|
-
concurrency: #{worker_upgrade_concurrency}
|
630
|
-
version: #{new_k3s_version}
|
631
|
-
nodeSelector:
|
632
|
-
matchExpressions:
|
633
|
-
- {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
|
634
|
-
serviceAccountName: system-upgrade
|
635
|
-
prepare:
|
636
|
-
image: rancher/k3s-upgrade
|
637
|
-
args: ["prepare", "k3s-server"]
|
638
|
-
cordon: true
|
639
|
-
upgrade:
|
640
|
-
image: rancher/k3s-upgrade
|
641
|
-
EOF
|
642
|
-
|
643
|
-
temp_file_path = "/tmp/k3s-upgrade-plan.yaml"
|
644
|
-
|
645
|
-
File.write(temp_file_path, manifest)
|
646
|
-
|
647
|
-
temp_file_path
|
648
|
-
end
|
649
|
-
|
650
518
|
def belongs_to_cluster?(server)
|
651
519
|
server.dig("labels", "cluster") == cluster_name
|
652
520
|
end
|
@@ -656,4 +524,19 @@ class Cluster
|
|
656
524
|
schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
|
657
525
|
end
|
658
526
|
|
527
|
+
def image
|
528
|
+
configuration.dig("image") || "ubuntu-20.04"
|
529
|
+
end
|
530
|
+
|
531
|
+
def additional_packages
|
532
|
+
configuration.dig("additional_packages") || []
|
533
|
+
end
|
534
|
+
|
535
|
+
def check_kubectl
|
536
|
+
unless which("kubectl")
|
537
|
+
puts "Please ensure kubectl is installed and in your PATH."
|
538
|
+
exit 1
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
659
542
|
end
|
data/lib/hetzner/k3s/version.rb
CHANGED
@@ -0,0 +1,99 @@
|
|
1
|
+
module Utils
|
2
|
+
def which(cmd)
|
3
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
4
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
5
|
+
exts.each do |ext|
|
6
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
7
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(command, kubeconfig_path:)
|
14
|
+
env = ENV.to_hash.merge({
|
15
|
+
"KUBECONFIG" => kubeconfig_path
|
16
|
+
})
|
17
|
+
|
18
|
+
cmd_path = "/tmp/cli.cmd"
|
19
|
+
|
20
|
+
File.open(cmd_path, "w") do |f|
|
21
|
+
f.write("set -euo pipefail\n")
|
22
|
+
f.write(command)
|
23
|
+
end
|
24
|
+
|
25
|
+
FileUtils.chmod("+x", cmd_path)
|
26
|
+
|
27
|
+
begin
|
28
|
+
process = nil
|
29
|
+
|
30
|
+
at_exit do
|
31
|
+
begin
|
32
|
+
process&.send_signal("SIGTERM")
|
33
|
+
rescue Errno::ESRCH, Interrupt
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Subprocess.check_call(["bash", "-c", cmd_path], env: env) do |p|
|
38
|
+
process = p
|
39
|
+
end
|
40
|
+
|
41
|
+
rescue Subprocess::NonZeroExit
|
42
|
+
puts "Command failed: non-zero exit code"
|
43
|
+
exit 1
|
44
|
+
rescue Interrupt
|
45
|
+
puts "Command interrupted"
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def wait_for_ssh(server)
|
51
|
+
Timeout::timeout(5) do
|
52
|
+
server_name = server["name"]
|
53
|
+
|
54
|
+
puts "Waiting for server #{server_name} to be up..."
|
55
|
+
|
56
|
+
loop do
|
57
|
+
result = ssh(server, "echo UP")
|
58
|
+
break if result == "UP"
|
59
|
+
end
|
60
|
+
|
61
|
+
puts "...server #{server_name} is now up."
|
62
|
+
end
|
63
|
+
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error, IOError
|
64
|
+
retry
|
65
|
+
end
|
66
|
+
|
67
|
+
def ssh(server, command, print_output: false)
|
68
|
+
public_ip = server.dig("public_net", "ipv4", "ip")
|
69
|
+
output = ""
|
70
|
+
|
71
|
+
params = { verify_host_key: (verify_host_key ? :always : :never) }
|
72
|
+
|
73
|
+
if private_ssh_key_path
|
74
|
+
params[:keys] = [private_ssh_key_path]
|
75
|
+
end
|
76
|
+
|
77
|
+
Net::SSH.start(public_ip, "root", params) do |session|
|
78
|
+
session.exec!(command) do |channel, stream, data|
|
79
|
+
output << data
|
80
|
+
puts data if print_output
|
81
|
+
end
|
82
|
+
end
|
83
|
+
output.chop
|
84
|
+
rescue Net::SSH::Disconnect => e
|
85
|
+
retry unless e.message =~ /Too many authentication failures/
|
86
|
+
rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
|
87
|
+
retry
|
88
|
+
rescue Net::SSH::AuthenticationFailed
|
89
|
+
puts
|
90
|
+
puts "Cannot continue: SSH authentication failed. Please ensure that the private SSH key is correct."
|
91
|
+
exit 1
|
92
|
+
rescue Net::SSH::HostKeyMismatch
|
93
|
+
puts
|
94
|
+
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."
|
95
|
+
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."
|
96
|
+
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."
|
97
|
+
exit 1
|
98
|
+
end
|
99
|
+
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.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vito Botta
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: sshkey
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: ed25519
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -81,7 +81,7 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: bcrypt_pbkdf
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
@@ -95,7 +95,7 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: subprocess
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
@@ -119,6 +119,7 @@ extra_rdoc_files: []
|
|
119
119
|
files:
|
120
120
|
- ".gitignore"
|
121
121
|
- ".rspec"
|
122
|
+
- ".ruby-version"
|
122
123
|
- ".travis.yml"
|
123
124
|
- CODE_OF_CONDUCT.md
|
124
125
|
- Dockerfile
|
@@ -144,9 +145,9 @@ files:
|
|
144
145
|
- lib/hetzner/infra/server.rb
|
145
146
|
- lib/hetzner/infra/ssh_key.rb
|
146
147
|
- lib/hetzner/k3s/cli.rb
|
147
|
-
- lib/hetzner/k3s/client_patch.rb
|
148
148
|
- lib/hetzner/k3s/cluster.rb
|
149
149
|
- lib/hetzner/k3s/version.rb
|
150
|
+
- lib/hetzner/utils.rb
|
150
151
|
homepage: https://github.com/vitobotta/hetzner-k3s
|
151
152
|
licenses:
|
152
153
|
- MIT
|
@@ -154,7 +155,7 @@ metadata:
|
|
154
155
|
homepage_uri: https://github.com/vitobotta/hetzner-k3s
|
155
156
|
source_code_uri: https://github.com/vitobotta/hetzner-k3s
|
156
157
|
changelog_uri: https://github.com/vitobotta/hetzner-k3s
|
157
|
-
post_install_message:
|
158
|
+
post_install_message:
|
158
159
|
rdoc_options: []
|
159
160
|
require_paths:
|
160
161
|
- lib
|
@@ -169,8 +170,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
169
170
|
- !ruby/object:Gem::Version
|
170
171
|
version: '0'
|
171
172
|
requirements: []
|
172
|
-
rubygems_version: 3.
|
173
|
-
signing_key:
|
173
|
+
rubygems_version: 3.3.3
|
174
|
+
signing_key:
|
174
175
|
specification_version: 4
|
175
176
|
summary: A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using
|
176
177
|
k3s.
|
@@ -1,38 +0,0 @@
|
|
1
|
-
module K8s
|
2
|
-
class ResourceClient
|
3
|
-
def initialize(transport, api_client, api_resource, namespace: nil, resource_class: K8s::Resource)
|
4
|
-
@transport = transport
|
5
|
-
@api_client = api_client
|
6
|
-
@api_resource = api_resource
|
7
|
-
@namespace = namespace
|
8
|
-
@resource_class = resource_class
|
9
|
-
|
10
|
-
if @api_resource.name.include? '/'
|
11
|
-
@resource, @subresource = @api_resource.name.split('/', 2)
|
12
|
-
else
|
13
|
-
@resource = @api_resource.name
|
14
|
-
@subresource = nil
|
15
|
-
end
|
16
|
-
|
17
|
-
# fail "Resource #{api_resource.name} is not namespaced" unless api_resource.namespaced || !namespace
|
18
|
-
end
|
19
|
-
|
20
|
-
def path(name = nil, subresource: @subresource, namespace: @namespace)
|
21
|
-
namespace_part = namespace ? ['namespaces', namespace] : []
|
22
|
-
|
23
|
-
if namespaced?
|
24
|
-
if name && subresource
|
25
|
-
@api_client.path(*namespace_part, @resource, name, subresource)
|
26
|
-
elsif name
|
27
|
-
@api_client.path(*namespace_part, @resource, name)
|
28
|
-
else namespaced?
|
29
|
-
@api_client.path(*namespace_part, @resource)
|
30
|
-
end
|
31
|
-
elsif name
|
32
|
-
@api_client.path(@resource, name)
|
33
|
-
else
|
34
|
-
@api_client.path(@resource)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|