hetzner-k3s 0.4.8 → 0.4.9
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/Gemfile.lock +5 -51
- data/README.md +9 -3
- data/bin/build.sh +3 -3
- data/hetzner-k3s.gemspec +1 -1
- data/lib/hetzner/k3s/cli.rb +9 -47
- data/lib/hetzner/k3s/cluster.rb +114 -246
- 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: 22358cdc272faa5e09ae2f561bf4225990a87b0461f4920439f9d5e9543fbe59
|
4
|
+
data.tar.gz: fc7dc822d53cd881e01a18c509d666bdd0321ed25c636f2873bca3fa1e5e0ce9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 35a06d127f14f4848a6a87611292b67e0edbf6cc3fb1ae8b803214428369cfc519942702fe36c14c2537732b38fa024b5d2361f77db56ea58724446f4822537d
|
7
|
+
data.tar.gz: bfc7751afa7db09a5e929b8164cdab060dfcb7b86125ae30a4804e179ba8a0550bdb07c9a181e148284e10ef7a632fe26ad81da1e278aeeab492a2899e2cdffb
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.1.0
|
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
|
-
excon (0.88.0)
|
51
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.4.
|
41
|
+
docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.4.9 create-cluster --config-file /cluster/test.yaml
|
42
42
|
```
|
43
43
|
|
44
44
|
Replace `test.yaml` with the name of your config file.
|
@@ -108,7 +108,7 @@ curl \
|
|
108
108
|
'https://api.hetzner.cloud/v1/images'
|
109
109
|
```
|
110
110
|
|
111
|
-
Note that if you use a custom image, the creation of the servers may take longer than when using the default image.
|
111
|
+
Note that if you use a custom image, the creation of the servers may take longer than when using the default image.
|
112
112
|
|
113
113
|
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.
|
114
114
|
|
@@ -211,7 +211,7 @@ kubectl label node <master1> <master2> <master2> plan.upgrade.cattle.io/k3s-serv
|
|
211
211
|
To delete a cluster, running
|
212
212
|
|
213
213
|
```bash
|
214
|
-
hetzner-k3s delete-cluster --config-file cluster_config.
|
214
|
+
hetzner-k3s delete-cluster --config-file cluster_config.yaml
|
215
215
|
```
|
216
216
|
|
217
217
|
This will delete all the resources in the Hetzner Cloud project for the cluster being deleted.
|
@@ -254,6 +254,12 @@ I recommend that you create a separate Hetzner project for each cluster, because
|
|
254
254
|
|
255
255
|
## changelog
|
256
256
|
|
257
|
+
- 0.4.9
|
258
|
+
- Ensure the program always exits with exit code 1 if the config file fails validation
|
259
|
+
- Upgrade System Upgrade Controller to 0.8.1
|
260
|
+
- Remove dependency on unmaintained gem k8s-ruby
|
261
|
+
- Make the gem compatible with Ruby 3.1.0
|
262
|
+
|
257
263
|
- 0.4.8
|
258
264
|
- Increase timeout with API requests to 30 seconds
|
259
265
|
- Limit number of retries for API requests to 3
|
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}:
|
9
|
+
docker build -t ${IMAGE}:v9 \
|
10
10
|
--platform=linux/amd64 \
|
11
|
-
--cache-from ${IMAGE}:v0.4.
|
11
|
+
--cache-from ${IMAGE}:v0.4.8 \
|
12
12
|
--build-arg BUILDKIT_INLINE_CACHE=1 .
|
13
13
|
|
14
|
-
docker push vitobotta/hetzner-k3s:v0.4.
|
14
|
+
docker push vitobotta/hetzner-k3s:v0.4.9
|
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/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 = []
|
@@ -96,7 +100,6 @@ module Hetzner
|
|
96
100
|
when :upgrade
|
97
101
|
validate_kubeconfig_path_must_exist
|
98
102
|
validate_new_k3s_version
|
99
|
-
validate_new_k3s_version_must_be_more_recent
|
100
103
|
end
|
101
104
|
|
102
105
|
errors.flatten!
|
@@ -271,36 +274,6 @@ module Hetzner
|
|
271
274
|
schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
|
272
275
|
end
|
273
276
|
|
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
277
|
def validate_instance_group(instance_group, workers: true)
|
305
278
|
instance_group_errors = []
|
306
279
|
|
@@ -333,17 +306,6 @@ module Hetzner
|
|
333
306
|
errors << instance_group_errors
|
334
307
|
end
|
335
308
|
|
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
309
|
def validate_verify_host_key
|
348
310
|
return unless [true, false].include?(configuration.fetch("public_ssh_key_path", false))
|
349
311
|
errors << "Please set the verify_host_key option to either true or false"
|
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
|
@@ -76,7 +78,7 @@ class Cluster
|
|
76
78
|
|
77
79
|
attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
|
78
80
|
:masters_config, :worker_node_pools,
|
79
|
-
:location, :public_ssh_key_path,
|
81
|
+
:location, :public_ssh_key_path,
|
80
82
|
:hetzner_token, :tls_sans, :new_k3s_version, :configuration,
|
81
83
|
:config_file, :verify_host_key, :networks, :private_ssh_key_path, :configuration
|
82
84
|
|
@@ -153,7 +155,7 @@ class Cluster
|
|
153
155
|
|
154
156
|
threads = server_configs.map do |server_config|
|
155
157
|
Thread.new do
|
156
|
-
servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(server_config)
|
158
|
+
servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(**server_config)
|
157
159
|
end
|
158
160
|
end
|
159
161
|
|
@@ -207,25 +209,71 @@ class Cluster
|
|
207
209
|
end
|
208
210
|
|
209
211
|
def upgrade_cluster
|
210
|
-
|
211
|
-
|
212
|
-
begin
|
213
|
-
kubernetes_client.api("upgrade.cattle.io/v1").resource("plans").get("k3s-server", namespace: "system-upgrade")
|
214
|
-
|
215
|
-
puts "Aborting - an upgrade is already in progress."
|
216
|
-
|
217
|
-
rescue K8s::Error::NotFound
|
218
|
-
resources.each do |resource|
|
219
|
-
kubernetes_client.create_resource(resource)
|
220
|
-
end
|
221
|
-
|
222
|
-
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."
|
223
|
-
puts "The API server may be briefly unavailable during the upgrade of the controlplane."
|
224
|
-
|
225
|
-
configuration["k3s_version"] = new_k3s_version
|
212
|
+
worker_upgrade_concurrency = workers.size - 1
|
213
|
+
worker_upgrade_concurrency = 1 if worker_upgrade_concurrency == 0
|
226
214
|
|
227
|
-
|
228
|
-
|
215
|
+
cmd = <<~EOS
|
216
|
+
kubectl apply -f - <<-EOF
|
217
|
+
apiVersion: upgrade.cattle.io/v1
|
218
|
+
kind: Plan
|
219
|
+
metadata:
|
220
|
+
name: k3s-server
|
221
|
+
namespace: system-upgrade
|
222
|
+
labels:
|
223
|
+
k3s-upgrade: server
|
224
|
+
spec:
|
225
|
+
concurrency: 1
|
226
|
+
version: #{new_k3s_version}
|
227
|
+
nodeSelector:
|
228
|
+
matchExpressions:
|
229
|
+
- {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
|
230
|
+
serviceAccountName: system-upgrade
|
231
|
+
tolerations:
|
232
|
+
- key: "CriticalAddonsOnly"
|
233
|
+
operator: "Equal"
|
234
|
+
value: "true"
|
235
|
+
effect: "NoExecute"
|
236
|
+
cordon: true
|
237
|
+
upgrade:
|
238
|
+
image: rancher/k3s-upgrade
|
239
|
+
EOF
|
240
|
+
EOS
|
241
|
+
|
242
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
243
|
+
|
244
|
+
cmd = <<~EOS
|
245
|
+
kubectl apply -f - <<-EOF
|
246
|
+
apiVersion: upgrade.cattle.io/v1
|
247
|
+
kind: Plan
|
248
|
+
metadata:
|
249
|
+
name: k3s-agent
|
250
|
+
namespace: system-upgrade
|
251
|
+
labels:
|
252
|
+
k3s-upgrade: agent
|
253
|
+
spec:
|
254
|
+
concurrency: #{worker_upgrade_concurrency}
|
255
|
+
version: #{new_k3s_version}
|
256
|
+
nodeSelector:
|
257
|
+
matchExpressions:
|
258
|
+
- {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
|
259
|
+
serviceAccountName: system-upgrade
|
260
|
+
prepare:
|
261
|
+
image: rancher/k3s-upgrade
|
262
|
+
args: ["prepare", "k3s-server"]
|
263
|
+
cordon: true
|
264
|
+
upgrade:
|
265
|
+
image: rancher/k3s-upgrade
|
266
|
+
EOF
|
267
|
+
EOS
|
268
|
+
|
269
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
270
|
+
|
271
|
+
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."
|
272
|
+
puts "The API server may be briefly unavailable during the upgrade of the controlplane."
|
273
|
+
|
274
|
+
configuration["k3s_version"] = new_k3s_version
|
275
|
+
|
276
|
+
File.write(config_file, configuration.to_yaml)
|
229
277
|
end
|
230
278
|
|
231
279
|
|
@@ -317,201 +365,71 @@ class Cluster
|
|
317
365
|
end
|
318
366
|
|
319
367
|
def deploy_cloud_controller_manager
|
368
|
+
check_kubectl
|
369
|
+
|
320
370
|
puts
|
321
371
|
puts "Deploying Hetzner Cloud Controller Manager..."
|
322
372
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
network: Base64.encode64(cluster_name),
|
336
|
-
token: Base64.encode64(hetzner_token)
|
337
|
-
}
|
338
|
-
)
|
339
|
-
|
340
|
-
kubernetes_client.api('v1').resource('secrets').create_resource(secret)
|
341
|
-
end
|
342
|
-
|
373
|
+
cmd = <<~EOS
|
374
|
+
kubectl apply -f - <<-EOF
|
375
|
+
apiVersion: "v1"
|
376
|
+
kind: "Secret"
|
377
|
+
metadata:
|
378
|
+
namespace: 'kube-system'
|
379
|
+
name: 'hcloud'
|
380
|
+
stringData:
|
381
|
+
network: "#{cluster_name}"
|
382
|
+
token: "#{hetzner_token}"
|
383
|
+
EOF
|
384
|
+
EOS
|
343
385
|
|
344
|
-
|
345
|
-
|
346
|
-
File.write("/tmp/cloud-controller-manager.yaml", manifest)
|
347
|
-
|
348
|
-
resources = K8s::Resource.from_files("/tmp/cloud-controller-manager.yaml")
|
349
|
-
|
350
|
-
begin
|
351
|
-
kubernetes_client.api("apps/v1").resource("deployments").get("hcloud-cloud-controller-manager", namespace: "kube-system")
|
352
|
-
|
353
|
-
resources.each do |resource|
|
354
|
-
kubernetes_client.update_resource(resource)
|
355
|
-
end
|
386
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
356
387
|
|
357
|
-
|
358
|
-
resources.each do |resource|
|
359
|
-
kubernetes_client.create_resource(resource)
|
360
|
-
end
|
388
|
+
cmd = "kubectl apply -f https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml"
|
361
389
|
|
362
|
-
|
390
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
363
391
|
|
364
392
|
puts "...Cloud Controller Manager deployed"
|
365
|
-
rescue Excon::Error::Socket
|
366
|
-
retry
|
367
|
-
end
|
368
|
-
|
369
|
-
def fetch_manifest(url)
|
370
|
-
retries ||= 1
|
371
|
-
HTTP.follow.get(url).body
|
372
|
-
rescue
|
373
|
-
retry if (retries += 1) <= 10
|
374
393
|
end
|
375
394
|
|
376
395
|
def deploy_system_upgrade_controller
|
396
|
+
check_kubectl
|
397
|
+
|
377
398
|
puts
|
378
399
|
puts "Deploying k3s System Upgrade Controller..."
|
379
400
|
|
380
|
-
|
381
|
-
|
382
|
-
File.write("/tmp/system-upgrade-controller.yaml", manifest)
|
383
|
-
|
384
|
-
resources = K8s::Resource.from_files("/tmp/system-upgrade-controller.yaml")
|
385
|
-
|
386
|
-
begin
|
387
|
-
kubernetes_client.api("apps/v1").resource("deployments").get("system-upgrade-controller", namespace: "system-upgrade")
|
388
|
-
|
389
|
-
resources.each do |resource|
|
390
|
-
kubernetes_client.update_resource(resource)
|
391
|
-
end
|
392
|
-
|
393
|
-
rescue K8s::Error::NotFound
|
394
|
-
resources.each do |resource|
|
395
|
-
kubernetes_client.create_resource(resource)
|
396
|
-
end
|
401
|
+
cmd = "kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.1/system-upgrade-controller.yaml"
|
397
402
|
|
398
|
-
|
403
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
399
404
|
|
400
405
|
puts "...k3s System Upgrade Controller deployed"
|
401
|
-
rescue Excon::Error::Socket
|
402
|
-
retry
|
403
406
|
end
|
404
407
|
|
405
408
|
def deploy_csi_driver
|
409
|
+
check_kubectl
|
410
|
+
|
406
411
|
puts
|
407
412
|
puts "Deploying Hetzner CSI Driver..."
|
408
413
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
data: {
|
421
|
-
token: Base64.encode64(hetzner_token)
|
422
|
-
}
|
423
|
-
)
|
424
|
-
|
425
|
-
kubernetes_client.api('v1').resource('secrets').create_resource(secret)
|
426
|
-
end
|
427
|
-
|
428
|
-
|
429
|
-
manifest = HTTP.follow.get("https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml").body
|
430
|
-
|
431
|
-
File.write("/tmp/csi-driver.yaml", manifest)
|
414
|
+
cmd = <<~EOS
|
415
|
+
kubectl apply -f - <<-EOF
|
416
|
+
apiVersion: "v1"
|
417
|
+
kind: "Secret"
|
418
|
+
metadata:
|
419
|
+
namespace: 'kube-system'
|
420
|
+
name: 'hcloud-csi'
|
421
|
+
stringData:
|
422
|
+
token: "#{hetzner_token}"
|
423
|
+
EOF
|
424
|
+
EOS
|
432
425
|
|
433
|
-
|
426
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
434
427
|
|
435
|
-
|
436
|
-
kubernetes_client.api("apps/v1").resource("daemonsets").get("hcloud-csi-node", namespace: "kube-system")
|
428
|
+
cmd = "kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml"
|
437
429
|
|
438
|
-
|
439
|
-
resources.each do |resource|
|
440
|
-
begin
|
441
|
-
kubernetes_client.update_resource(resource)
|
442
|
-
rescue K8s::Error::Invalid => e
|
443
|
-
raise e unless e.message =~ /must be specified/i
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
|
-
rescue K8s::Error::NotFound
|
448
|
-
resources.each do |resource|
|
449
|
-
kubernetes_client.create_resource(resource)
|
450
|
-
end
|
451
|
-
|
452
|
-
end
|
430
|
+
run cmd, kubeconfig_path: kubeconfig_path
|
453
431
|
|
454
432
|
puts "...CSI Driver deployed"
|
455
|
-
rescue Excon::Error::Socket
|
456
|
-
retry
|
457
|
-
end
|
458
|
-
|
459
|
-
def wait_for_ssh(server)
|
460
|
-
Timeout::timeout(5) do
|
461
|
-
server_name = server["name"]
|
462
|
-
|
463
|
-
puts "Waiting for server #{server_name} to be up..."
|
464
|
-
|
465
|
-
loop do
|
466
|
-
result = ssh(server, "echo UP")
|
467
|
-
break if result == "UP"
|
468
|
-
end
|
469
|
-
|
470
|
-
puts "...server #{server_name} is now up."
|
471
|
-
end
|
472
|
-
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error, IOError
|
473
|
-
retry
|
474
|
-
end
|
475
|
-
|
476
|
-
def ssh(server, command, print_output: false)
|
477
|
-
public_ip = server.dig("public_net", "ipv4", "ip")
|
478
|
-
output = ""
|
479
|
-
|
480
|
-
params = { verify_host_key: (verify_host_key ? :always : :never) }
|
481
|
-
|
482
|
-
if private_ssh_key_path
|
483
|
-
params[:keys] = [private_ssh_key_path]
|
484
|
-
end
|
485
|
-
|
486
|
-
Net::SSH.start(public_ip, "root", params) do |session|
|
487
|
-
session.exec!(command) do |channel, stream, data|
|
488
|
-
output << data
|
489
|
-
puts data if print_output
|
490
|
-
end
|
491
|
-
end
|
492
|
-
output.chop
|
493
|
-
rescue Net::SSH::Disconnect => e
|
494
|
-
retry unless e.message =~ /Too many authentication failures/
|
495
|
-
rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
|
496
|
-
retry
|
497
|
-
rescue Net::SSH::AuthenticationFailed
|
498
|
-
puts
|
499
|
-
puts "Cannot continue: SSH authentication failed. Please ensure that the private SSH key is correct."
|
500
|
-
exit 1
|
501
|
-
rescue Net::SSH::HostKeyMismatch
|
502
|
-
puts
|
503
|
-
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."
|
504
|
-
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."
|
505
|
-
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."
|
506
|
-
exit 1
|
507
|
-
end
|
508
|
-
|
509
|
-
def kubernetes_client
|
510
|
-
return @kubernetes_client if @kubernetes_client
|
511
|
-
|
512
|
-
config_hash = YAML.load_file(kubeconfig_path)
|
513
|
-
config_hash['current-context'] = cluster_name
|
514
|
-
@kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
|
515
433
|
end
|
516
434
|
|
517
435
|
def find_flannel_interface(server)
|
@@ -591,63 +509,6 @@ class Cluster
|
|
591
509
|
FileUtils.chmod "go-r", kubeconfig_path
|
592
510
|
end
|
593
511
|
|
594
|
-
def ugrade_plan_manifest_path
|
595
|
-
worker_upgrade_concurrency = workers.size - 1
|
596
|
-
worker_upgrade_concurrency = 1 if worker_upgrade_concurrency == 0
|
597
|
-
|
598
|
-
manifest = <<~EOF
|
599
|
-
apiVersion: upgrade.cattle.io/v1
|
600
|
-
kind: Plan
|
601
|
-
metadata:
|
602
|
-
name: k3s-server
|
603
|
-
namespace: system-upgrade
|
604
|
-
labels:
|
605
|
-
k3s-upgrade: server
|
606
|
-
spec:
|
607
|
-
concurrency: 1
|
608
|
-
version: #{new_k3s_version}
|
609
|
-
nodeSelector:
|
610
|
-
matchExpressions:
|
611
|
-
- {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
|
612
|
-
serviceAccountName: system-upgrade
|
613
|
-
tolerations:
|
614
|
-
- key: "CriticalAddonsOnly"
|
615
|
-
operator: "Equal"
|
616
|
-
value: "true"
|
617
|
-
effect: "NoExecute"
|
618
|
-
cordon: true
|
619
|
-
upgrade:
|
620
|
-
image: rancher/k3s-upgrade
|
621
|
-
---
|
622
|
-
apiVersion: upgrade.cattle.io/v1
|
623
|
-
kind: Plan
|
624
|
-
metadata:
|
625
|
-
name: k3s-agent
|
626
|
-
namespace: system-upgrade
|
627
|
-
labels:
|
628
|
-
k3s-upgrade: agent
|
629
|
-
spec:
|
630
|
-
concurrency: #{worker_upgrade_concurrency}
|
631
|
-
version: #{new_k3s_version}
|
632
|
-
nodeSelector:
|
633
|
-
matchExpressions:
|
634
|
-
- {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
|
635
|
-
serviceAccountName: system-upgrade
|
636
|
-
prepare:
|
637
|
-
image: rancher/k3s-upgrade
|
638
|
-
args: ["prepare", "k3s-server"]
|
639
|
-
cordon: true
|
640
|
-
upgrade:
|
641
|
-
image: rancher/k3s-upgrade
|
642
|
-
EOF
|
643
|
-
|
644
|
-
temp_file_path = "/tmp/k3s-upgrade-plan.yaml"
|
645
|
-
|
646
|
-
File.write(temp_file_path, manifest)
|
647
|
-
|
648
|
-
temp_file_path
|
649
|
-
end
|
650
|
-
|
651
512
|
def belongs_to_cluster?(server)
|
652
513
|
server.dig("labels", "cluster") == cluster_name
|
653
514
|
end
|
@@ -661,4 +522,11 @@ class Cluster
|
|
661
522
|
configuration.dig("image") || "ubuntu-20.04"
|
662
523
|
end
|
663
524
|
|
525
|
+
def check_kubectl
|
526
|
+
unless which("kubectl")
|
527
|
+
puts "Please ensure kubectl is installed and in your PATH."
|
528
|
+
exit 1
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
664
532
|
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.
|
4
|
+
version: 0.4.9
|
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
|