hetzner-k3s 0.3.6 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Dockerfile +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +28 -1
- data/lib/hetzner/infra/firewall.rb +79 -57
- data/lib/hetzner/infra/load_balancer.rb +14 -2
- data/lib/hetzner/k3s/cli.rb +86 -13
- data/lib/hetzner/k3s/cluster.rb +25 -44
- data/lib/hetzner/k3s/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ee4a4ac2c31ebff805ee20edc3658ffe64be32e50b524ee4af3646e3ffc3a3c
|
4
|
+
data.tar.gz: 8cbc33a2a696b19c8e614932d1daa7fa9beddaf9d69dd8377d909cb382e40f87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff2ca466abbd198b3bc76c8854113d90033fb606e9f11152ecf6d079564ee4dcbdab359a5b17229770a7dc531a9674b211d079a2204596efe3ec5b67157bf82e
|
7
|
+
data.tar.gz: a6a16c64b0ada5c4d1a740894df09a9f41ed0110b06cfd5629b1971c64836a5fd38bceebb7898432b275cb677779606ecd780b917d4095eb161987d26c0eecc0
|
data/Dockerfile
CHANGED
data/Gemfile.lock
CHANGED
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 create-cluster --config-file /cluster/test.yaml
|
41
|
+
docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.3.8 create-cluster --config-file /cluster/test.yaml
|
42
42
|
```
|
43
43
|
|
44
44
|
Replace `test.yaml` with the name of your config file.
|
@@ -54,6 +54,8 @@ cluster_name: test
|
|
54
54
|
kubeconfig_path: "./kubeconfig"
|
55
55
|
k3s_version: v1.21.3+k3s1
|
56
56
|
ssh_key_path: "~/.ssh/id_rsa.pub"
|
57
|
+
ssh_allowed_networks:
|
58
|
+
- 0.0.0.0/0
|
57
59
|
verify_host_key: false
|
58
60
|
location: nbg1
|
59
61
|
masters:
|
@@ -72,6 +74,11 @@ It should hopefully be self explanatory; you can run `hetzner-k3s releases` to s
|
|
72
74
|
|
73
75
|
If you are using Docker, then set `kubeconfig_path` to `/cluster/kubeconfig` so that the kubeconfig is created in the same directory where your config file is.
|
74
76
|
|
77
|
+
If you don't want to specify the Hetzner token in the config file (for example if you want to use the tool with CI), then you can use the `HCLOUD_TOKEN` environment variable instead, which has predecence.
|
78
|
+
|
79
|
+
**Important**: The tool assignes the label `cluster` to each server it creates, with the clsuter name you specify in the config file, as the value. So please ensure you don't create unrelated servers in the same project having
|
80
|
+
the label `cluster=<cluster name>`, because otherwise they will be deleted if you delete the cluster. I recommend you create a separate Hetzner project for each cluster, see note at the end of this README for more details.
|
81
|
+
|
75
82
|
|
76
83
|
If you set `masters.instance_count` to 1 then the tool will create a non highly available control plane; for production clusters you may want to set it to a number greater than 1. This number must be odd to avoid split brain issues with etcd and the recommended number is 3.
|
77
84
|
|
@@ -225,8 +232,28 @@ The other annotations should be self explanatory. You can find a list of the ava
|
|
225
232
|
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.
|
226
233
|
|
227
234
|
|
235
|
+
## Keeping a project per cluster
|
236
|
+
|
237
|
+
I recommend that you create a separate Hetzner project for each cluster, because otherwise multiple clusters will attempt to create overlapping routes. I will make the pod cidr configurable in the future to avoid this, but I still recommend keeping clusters separated from each other. This way, if you want to delete a cluster with all the resources created for it, you can just delete the project.
|
238
|
+
|
239
|
+
|
228
240
|
## changelog
|
229
241
|
|
242
|
+
- 0.4.0
|
243
|
+
- Ensure the masters are removed from the API load balancer before deleting the load balancer
|
244
|
+
- Ensure the servers are removed from the firewall before deleting it
|
245
|
+
- Allow using an environment variable to specify the Hetzner token
|
246
|
+
- Allow restricting SSH access to the nodes to specific networks
|
247
|
+
|
248
|
+
- 0.3.9
|
249
|
+
- Add command "version" to print the version of the tool in use
|
250
|
+
|
251
|
+
- 0.3.8
|
252
|
+
- Fix: added a check on a label to ensure that only servers that belong to the cluster are deleted from the project
|
253
|
+
|
254
|
+
- 0.3.7
|
255
|
+
- Ensure that the cluster name only contains lowercase letters, digits and dashes for compatibility with the cloud controller manager
|
256
|
+
|
230
257
|
- 0.3.6
|
231
258
|
- Retry SSH commands when IO errors occur
|
232
259
|
|
@@ -5,7 +5,9 @@ module Hetzner
|
|
5
5
|
@cluster_name = cluster_name
|
6
6
|
end
|
7
7
|
|
8
|
-
def create
|
8
|
+
def create(ha:, networks:)
|
9
|
+
@ha = ha
|
10
|
+
@networks = networks
|
9
11
|
puts
|
10
12
|
|
11
13
|
if firewall = find_firewall
|
@@ -16,16 +18,21 @@ module Hetzner
|
|
16
18
|
|
17
19
|
puts "Creating firewall..."
|
18
20
|
|
19
|
-
response = hetzner_client.post("/firewalls",
|
21
|
+
response = hetzner_client.post("/firewalls", create_firewall_config).body
|
20
22
|
puts "...firewall created."
|
21
23
|
puts
|
22
24
|
|
23
25
|
JSON.parse(response)["firewall"]["id"]
|
24
26
|
end
|
25
27
|
|
26
|
-
def delete
|
28
|
+
def delete(servers)
|
27
29
|
if firewall = find_firewall
|
28
30
|
puts "Deleting firewall..."
|
31
|
+
|
32
|
+
servers.each do |server|
|
33
|
+
hetzner_client.post("/firewalls/#{firewall["id"]}/actions/remove_from_resources", remove_targets_config(server["id"]))
|
34
|
+
end
|
35
|
+
|
29
36
|
hetzner_client.delete("/firewalls", firewall["id"])
|
30
37
|
puts "...firewall deleted."
|
31
38
|
else
|
@@ -37,64 +44,79 @@ module Hetzner
|
|
37
44
|
|
38
45
|
private
|
39
46
|
|
40
|
-
attr_reader :hetzner_client, :cluster_name, :firewall
|
47
|
+
attr_reader :hetzner_client, :cluster_name, :firewall, :ha, :networks
|
48
|
+
|
49
|
+
def create_firewall_config
|
50
|
+
rules = [
|
51
|
+
{
|
52
|
+
"description": "Allow port 22 (SSH)",
|
53
|
+
"direction": "in",
|
54
|
+
"protocol": "tcp",
|
55
|
+
"port": "22",
|
56
|
+
"source_ips": networks,
|
57
|
+
"destination_ips": []
|
58
|
+
},
|
59
|
+
{
|
60
|
+
"description": "Allow ICMP (ping)",
|
61
|
+
"direction": "in",
|
62
|
+
"protocol": "icmp",
|
63
|
+
"port": nil,
|
64
|
+
"source_ips": [
|
65
|
+
"0.0.0.0/0",
|
66
|
+
"::/0"
|
67
|
+
],
|
68
|
+
"destination_ips": []
|
69
|
+
},
|
70
|
+
{
|
71
|
+
"description": "Allow all TCP traffic between nodes on the private network",
|
72
|
+
"direction": "in",
|
73
|
+
"protocol": "tcp",
|
74
|
+
"port": "any",
|
75
|
+
"source_ips": [
|
76
|
+
"10.0.0.0/16"
|
77
|
+
],
|
78
|
+
"destination_ips": []
|
79
|
+
},
|
80
|
+
{
|
81
|
+
"description": "Allow all UDP traffic between nodes on the private network",
|
82
|
+
"direction": "in",
|
83
|
+
"protocol": "udp",
|
84
|
+
"port": "any",
|
85
|
+
"source_ips": [
|
86
|
+
"10.0.0.0/16"
|
87
|
+
],
|
88
|
+
"destination_ips": []
|
89
|
+
}
|
90
|
+
]
|
91
|
+
|
92
|
+
unless ha
|
93
|
+
rules << {
|
94
|
+
"description": "Allow port 6443 (Kubernetes API server)",
|
95
|
+
"direction": "in",
|
96
|
+
"protocol": "tcp",
|
97
|
+
"port": "6443",
|
98
|
+
"source_ips": [
|
99
|
+
"0.0.0.0/0",
|
100
|
+
"::/0"
|
101
|
+
],
|
102
|
+
"destination_ips": []
|
103
|
+
}
|
104
|
+
end
|
41
105
|
|
42
|
-
def firewall_config
|
43
106
|
{
|
44
107
|
name: cluster_name,
|
45
|
-
rules:
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
"0.0.0.0/0",
|
53
|
-
"::/0"
|
54
|
-
],
|
55
|
-
"destination_ips": []
|
56
|
-
},
|
57
|
-
{
|
58
|
-
"description": "Allow ICMP (ping)",
|
59
|
-
"direction": "in",
|
60
|
-
"protocol": "icmp",
|
61
|
-
"port": nil,
|
62
|
-
"source_ips": [
|
63
|
-
"0.0.0.0/0",
|
64
|
-
"::/0"
|
65
|
-
],
|
66
|
-
"destination_ips": []
|
67
|
-
},
|
68
|
-
{
|
69
|
-
"description": "Allow port 6443 (Kubernetes API server)",
|
70
|
-
"direction": "in",
|
71
|
-
"protocol": "tcp",
|
72
|
-
"port": "6443",
|
73
|
-
"source_ips": [
|
74
|
-
"0.0.0.0/0",
|
75
|
-
"::/0"
|
76
|
-
],
|
77
|
-
"destination_ips": []
|
78
|
-
},
|
79
|
-
{
|
80
|
-
"description": "Allow all TCP traffic between nodes on the private network",
|
81
|
-
"direction": "in",
|
82
|
-
"protocol": "tcp",
|
83
|
-
"port": "any",
|
84
|
-
"source_ips": [
|
85
|
-
"10.0.0.0/16"
|
86
|
-
],
|
87
|
-
"destination_ips": []
|
88
|
-
},
|
108
|
+
rules: rules
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def remove_targets_config(server_id)
|
113
|
+
{
|
114
|
+
"remove_from": [
|
89
115
|
{
|
90
|
-
"
|
91
|
-
|
92
|
-
|
93
|
-
"
|
94
|
-
"source_ips": [
|
95
|
-
"10.0.0.0/16"
|
96
|
-
],
|
97
|
-
"destination_ips": []
|
116
|
+
"server": {
|
117
|
+
"id": server_id
|
118
|
+
},
|
119
|
+
"type": "server"
|
98
120
|
}
|
99
121
|
]
|
100
122
|
}
|
@@ -19,7 +19,7 @@ module Hetzner
|
|
19
19
|
|
20
20
|
puts "Creating API load_balancer..."
|
21
21
|
|
22
|
-
response = hetzner_client.post("/load_balancers",
|
22
|
+
response = hetzner_client.post("/load_balancers", create_load_balancer_config).body
|
23
23
|
puts "...API load balancer created."
|
24
24
|
puts
|
25
25
|
|
@@ -29,6 +29,9 @@ module Hetzner
|
|
29
29
|
def delete(ha:)
|
30
30
|
if load_balancer = find_load_balancer
|
31
31
|
puts "Deleting API load balancer..." unless ha
|
32
|
+
|
33
|
+
hetzner_client.post("/load_balancers/#{load_balancer["id"]}/actions/remove_target", remove_targets_config)
|
34
|
+
|
32
35
|
hetzner_client.delete("/load_balancers", load_balancer["id"])
|
33
36
|
puts "...API load balancer deleted." unless ha
|
34
37
|
elsif ha
|
@@ -46,7 +49,7 @@ module Hetzner
|
|
46
49
|
"#{cluster_name}-api"
|
47
50
|
end
|
48
51
|
|
49
|
-
def
|
52
|
+
def create_load_balancer_config
|
50
53
|
{
|
51
54
|
"algorithm": {
|
52
55
|
"type": "round_robin"
|
@@ -76,6 +79,15 @@ module Hetzner
|
|
76
79
|
}
|
77
80
|
end
|
78
81
|
|
82
|
+
def remove_targets_config
|
83
|
+
{
|
84
|
+
"label_selector": {
|
85
|
+
"selector": "cluster=#{cluster_name},role=master"
|
86
|
+
},
|
87
|
+
"type": "label_selector"
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
79
91
|
def find_load_balancer
|
80
92
|
hetzner_client.get("/load_balancers")["load_balancers"].detect{ |load_balancer| load_balancer["name"] == load_balancer_name }
|
81
93
|
end
|
data/lib/hetzner/k3s/cli.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
require "thor"
|
2
2
|
require "http"
|
3
3
|
require "sshkey"
|
4
|
+
require 'ipaddr'
|
5
|
+
require 'open-uri'
|
4
6
|
|
5
7
|
require_relative "cluster"
|
8
|
+
require_relative "version"
|
6
9
|
|
7
10
|
module Hetzner
|
8
11
|
module K3s
|
@@ -11,13 +14,18 @@ module Hetzner
|
|
11
14
|
true
|
12
15
|
end
|
13
16
|
|
17
|
+
desc "version", "Print the version"
|
18
|
+
def version
|
19
|
+
puts Hetzner::K3s::VERSION
|
20
|
+
end
|
21
|
+
|
14
22
|
desc "create-cluster", "Create a k3s cluster in Hetzner Cloud"
|
15
23
|
option :config_file, required: true
|
16
24
|
|
17
25
|
def create_cluster
|
18
26
|
validate_config_file :create
|
19
27
|
|
20
|
-
Cluster.new(hetzner_client: hetzner_client).create configuration: configuration
|
28
|
+
Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).create configuration: configuration
|
21
29
|
end
|
22
30
|
|
23
31
|
desc "delete-cluster", "Delete an existing k3s cluster in Hetzner Cloud"
|
@@ -25,7 +33,7 @@ module Hetzner
|
|
25
33
|
|
26
34
|
def delete_cluster
|
27
35
|
validate_config_file :delete
|
28
|
-
Cluster.new(hetzner_client: hetzner_client).delete configuration: configuration
|
36
|
+
Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).delete configuration: configuration
|
29
37
|
end
|
30
38
|
|
31
39
|
desc "upgrade-cluster", "Upgrade an existing k3s cluster in Hetzner Cloud to a new version"
|
@@ -35,7 +43,7 @@ module Hetzner
|
|
35
43
|
|
36
44
|
def upgrade_cluster
|
37
45
|
validate_config_file :upgrade
|
38
|
-
Cluster.new(hetzner_client: hetzner_client).upgrade configuration: configuration, new_k3s_version: options[:new_k3s_version], config_file: options[:config_file]
|
46
|
+
Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).upgrade configuration: configuration, new_k3s_version: options[:new_k3s_version], config_file: options[:config_file]
|
39
47
|
end
|
40
48
|
|
41
49
|
desc "releases", "List available k3s releases"
|
@@ -76,6 +84,7 @@ module Hetzner
|
|
76
84
|
case action
|
77
85
|
when :create
|
78
86
|
validate_ssh_key
|
87
|
+
validate_ssh_allowed_networks
|
79
88
|
validate_location
|
80
89
|
validate_k3s_version
|
81
90
|
validate_masters
|
@@ -101,16 +110,31 @@ module Hetzner
|
|
101
110
|
end
|
102
111
|
end
|
103
112
|
|
113
|
+
def valid_token?
|
114
|
+
return @valid unless @valid.nil?
|
115
|
+
|
116
|
+
begin
|
117
|
+
token = find_hetzner_token
|
118
|
+
@hetzner_client = Hetzner::Client.new(token: token)
|
119
|
+
response = hetzner_client.get("/locations")
|
120
|
+
error_code = response.dig("error", "code")
|
121
|
+
@valid = if error_code and error_code.size > 0
|
122
|
+
false
|
123
|
+
else
|
124
|
+
true
|
125
|
+
end
|
126
|
+
rescue
|
127
|
+
@valid = false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
104
131
|
def validate_token
|
105
|
-
|
106
|
-
@hetzner_client = Hetzner::Client.new(token: token)
|
107
|
-
hetzner_client.get("/locations")
|
108
|
-
rescue
|
109
|
-
errors << "Invalid Hetzner Cloid token"
|
132
|
+
errors << "Invalid Hetzner Cloud token" unless valid_token?
|
110
133
|
end
|
111
134
|
|
112
135
|
def validate_cluster_name
|
113
|
-
errors << "Cluster name is an invalid format" unless configuration["cluster_name"] =~ /\A
|
136
|
+
errors << "Cluster name is an invalid format (only lowercase letters, digits and dashes are allowed)" unless configuration["cluster_name"] =~ /\A[a-z\d-]+\z/
|
137
|
+
errors << "Ensure that the cluster name starts with a normal letter" unless configuration["cluster_name"] =~ /\A[a-z]+.*\z/
|
114
138
|
end
|
115
139
|
|
116
140
|
def validate_kubeconfig_path
|
@@ -142,6 +166,7 @@ module Hetzner
|
|
142
166
|
end
|
143
167
|
|
144
168
|
def server_types
|
169
|
+
return [] unless valid_token?
|
145
170
|
@server_types ||= hetzner_client.get("/server_types")["server_types"].map{ |server_type| server_type["name"] }
|
146
171
|
rescue
|
147
172
|
@errors << "Cannot fetch server types with Hetzner API, please try again later"
|
@@ -149,13 +174,15 @@ module Hetzner
|
|
149
174
|
end
|
150
175
|
|
151
176
|
def locations
|
177
|
+
return [] unless valid_token?
|
152
178
|
@locations ||= hetzner_client.get("/locations")["locations"].map{ |location| location["name"] }
|
153
179
|
rescue
|
154
180
|
@errors << "Cannot fetch locations with Hetzner API, please try again later"
|
155
|
-
|
181
|
+
[]
|
156
182
|
end
|
157
183
|
|
158
184
|
def validate_location
|
185
|
+
return if locations.empty? && !valid_token?
|
159
186
|
errors << "Invalid location - available locations: nbg1 (Nuremberg, Germany), fsn1 (Falkenstein, Germany), hel1 (Helsinki, Finland)" unless locations.include? configuration.dig("location")
|
160
187
|
end
|
161
188
|
|
@@ -264,7 +291,7 @@ module Hetzner
|
|
264
291
|
instance_group_errors << "#{instance_group_type} is in an invalid format"
|
265
292
|
end
|
266
293
|
|
267
|
-
unless server_types.include?(instance_group["instance_type"])
|
294
|
+
unless !valid_token? or server_types.include?(instance_group["instance_type"])
|
268
295
|
instance_group_errors << "#{instance_group_type} has an invalid instance type"
|
269
296
|
end
|
270
297
|
|
@@ -289,16 +316,62 @@ module Hetzner
|
|
289
316
|
config_hash = YAML.load_file(File.expand_path(configuration["kubeconfig_path"]))
|
290
317
|
config_hash['current-context'] = configuration["cluster_name"]
|
291
318
|
@kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
|
292
|
-
rescue
|
293
319
|
errors << "Cannot connect to the Kubernetes cluster"
|
294
320
|
false
|
295
321
|
end
|
296
322
|
|
297
|
-
|
298
323
|
def validate_verify_host_key
|
299
324
|
return unless [true, false].include?(configuration.fetch("ssh_key_path", false))
|
300
325
|
errors << "Please set the verify_host_key option to either true or false"
|
301
326
|
end
|
327
|
+
|
328
|
+
def find_hetzner_token
|
329
|
+
@token = ENV["HCLOUD_TOKEN"]
|
330
|
+
return @token if @token
|
331
|
+
@token = configuration.dig("hetzner_token")
|
332
|
+
end
|
333
|
+
|
334
|
+
def validate_ssh_allowed_networks
|
335
|
+
networks ||= configuration.dig("ssh_allowed_networks")
|
336
|
+
|
337
|
+
if networks.nil? or networks.empty?
|
338
|
+
errors << "At least one network/IP range must be specified for SSH access"
|
339
|
+
return
|
340
|
+
end
|
341
|
+
|
342
|
+
invalid_networks = networks.reject do |network|
|
343
|
+
IPAddr.new(network) rescue false
|
344
|
+
end
|
345
|
+
|
346
|
+
unless invalid_networks.empty?
|
347
|
+
invalid_networks.each do |network|
|
348
|
+
errors << "The network #{network} is an invalid range"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
invalid_ranges = networks.reject do |network|
|
353
|
+
network.include? "/"
|
354
|
+
end
|
355
|
+
|
356
|
+
unless invalid_ranges.empty?
|
357
|
+
invalid_ranges.each do |network|
|
358
|
+
errors << "Please use the CIDR notation for the networks to avoid ambiguity"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
return unless invalid_networks.empty?
|
363
|
+
|
364
|
+
current_ip = URI.open('http://whatismyip.akamai.com').read
|
365
|
+
|
366
|
+
current_ip_networks = networks.detect do |network|
|
367
|
+
IPAddr.new(network).include?(current_ip) rescue false
|
368
|
+
end
|
369
|
+
|
370
|
+
unless current_ip_networks
|
371
|
+
errors << "Your current IP #{current_ip} is not included into any of the networks you've specified, so we won't be able to SSH into the nodes"
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
302
375
|
end
|
303
376
|
end
|
304
377
|
end
|
data/lib/hetzner/k3s/cluster.rb
CHANGED
@@ -16,12 +16,12 @@ require_relative "../k3s/client_patch"
|
|
16
16
|
|
17
17
|
|
18
18
|
class Cluster
|
19
|
-
def initialize(hetzner_client:)
|
19
|
+
def initialize(hetzner_client:, hetzner_token:)
|
20
20
|
@hetzner_client = hetzner_client
|
21
|
+
@hetzner_token = hetzner_token
|
21
22
|
end
|
22
23
|
|
23
24
|
def create(configuration:)
|
24
|
-
@hetzner_token = configuration.dig("hetzner_token")
|
25
25
|
@cluster_name = configuration.dig("cluster_name")
|
26
26
|
@kubeconfig_path = File.expand_path(configuration.dig("kubeconfig_path"))
|
27
27
|
@ssh_key_path = File.expand_path(configuration.dig("ssh_key_path"))
|
@@ -31,6 +31,7 @@ class Cluster
|
|
31
31
|
@location = configuration.dig("location")
|
32
32
|
@verify_host_key = configuration.fetch("verify_host_key", false)
|
33
33
|
@servers = []
|
34
|
+
@networks = configuration.dig("ssh_allowed_networks")
|
34
35
|
|
35
36
|
create_resources
|
36
37
|
|
@@ -69,7 +70,7 @@ class Cluster
|
|
69
70
|
:masters_config, :worker_node_pools,
|
70
71
|
:location, :ssh_key_path, :kubernetes_client,
|
71
72
|
:hetzner_token, :tls_sans, :new_k3s_version, :configuration,
|
72
|
-
:config_file, :verify_host_key
|
73
|
+
:config_file, :verify_host_key, :networks
|
73
74
|
|
74
75
|
|
75
76
|
def latest_k3s_version
|
@@ -78,10 +79,13 @@ class Cluster
|
|
78
79
|
end
|
79
80
|
|
80
81
|
def create_resources
|
82
|
+
master_instance_type = masters_config["instance_type"]
|
83
|
+
masters_count = masters_config["instance_count"]
|
84
|
+
|
81
85
|
firewall_id = Hetzner::Firewall.new(
|
82
86
|
hetzner_client: hetzner_client,
|
83
87
|
cluster_name: cluster_name
|
84
|
-
).create
|
88
|
+
).create(ha: (masters_count > 1), networks: networks)
|
85
89
|
|
86
90
|
network_id = Hetzner::Network.new(
|
87
91
|
hetzner_client: hetzner_client,
|
@@ -95,9 +99,6 @@ class Cluster
|
|
95
99
|
|
96
100
|
server_configs = []
|
97
101
|
|
98
|
-
master_instance_type = masters_config["instance_type"]
|
99
|
-
masters_count = masters_config["instance_count"]
|
100
|
-
|
101
102
|
masters_count.times do |i|
|
102
103
|
server_configs << {
|
103
104
|
location: location,
|
@@ -150,42 +151,15 @@ class Cluster
|
|
150
151
|
end
|
151
152
|
|
152
153
|
def delete_resources
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
threads = servers.map do |node|
|
159
|
-
Thread.new do
|
160
|
-
Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(server_name: node.metadata[:name])
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
threads.each(&:join) unless threads.empty?
|
165
|
-
end
|
166
|
-
rescue Timeout::Error, Excon::Error::Socket
|
167
|
-
puts "Unable to fetch nodes from Kubernetes API. Is the cluster online?"
|
168
|
-
end
|
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
|
-
|
181
|
-
puts
|
182
|
-
|
183
|
-
sleep 5 # give time for the servers to actually be deleted
|
154
|
+
Hetzner::LoadBalancer.new(
|
155
|
+
hetzner_client: hetzner_client,
|
156
|
+
cluster_name: cluster_name
|
157
|
+
).delete(ha: (masters.size > 1))
|
184
158
|
|
185
159
|
Hetzner::Firewall.new(
|
186
160
|
hetzner_client: hetzner_client,
|
187
161
|
cluster_name: cluster_name
|
188
|
-
).delete
|
162
|
+
).delete(all_servers)
|
189
163
|
|
190
164
|
Hetzner::Network.new(
|
191
165
|
hetzner_client: hetzner_client,
|
@@ -197,11 +171,13 @@ class Cluster
|
|
197
171
|
cluster_name: cluster_name
|
198
172
|
).delete(ssh_key_path: ssh_key_path)
|
199
173
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
174
|
+
threads = all_servers.map do |server|
|
175
|
+
Thread.new do
|
176
|
+
Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(server_name: server["name"])
|
177
|
+
end
|
178
|
+
end
|
204
179
|
|
180
|
+
threads.each(&:join) unless threads.empty?
|
205
181
|
end
|
206
182
|
|
207
183
|
def upgrade_cluster
|
@@ -249,6 +225,7 @@ class Cluster
|
|
249
225
|
--kube-scheduler-arg="bind-address=0.0.0.0" \
|
250
226
|
--node-taint CriticalAddonsOnly=true:NoExecute \
|
251
227
|
--kubelet-arg="cloud-provider=external" \
|
228
|
+
--advertise-address=$(hostname -I | awk '{print $2}') \
|
252
229
|
--node-ip=$(hostname -I | awk '{print $2}') \
|
253
230
|
--node-external-ip=$(hostname -I | awk '{print $1}') \
|
254
231
|
--flannel-iface=#{flannel_interface} \
|
@@ -501,7 +478,7 @@ class Cluster
|
|
501
478
|
end
|
502
479
|
|
503
480
|
def all_servers
|
504
|
-
@all_servers ||= hetzner_client.get("/servers")["servers"]
|
481
|
+
@all_servers ||= hetzner_client.get("/servers")["servers"].select{ |server| belongs_to_cluster?(server) == true }
|
505
482
|
end
|
506
483
|
|
507
484
|
def masters
|
@@ -624,4 +601,8 @@ class Cluster
|
|
624
601
|
temp_file_path
|
625
602
|
end
|
626
603
|
|
604
|
+
def belongs_to_cluster?(server)
|
605
|
+
server.dig("labels", "cluster") == cluster_name
|
606
|
+
end
|
607
|
+
|
627
608
|
end
|
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.4.0
|
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-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|