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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b1b2acb7f0649eac91750409418446c2b1ffea2b1b7a32b1f9f8a6fd2d50c5b
4
- data.tar.gz: 457994e28a028e8a1052e20a68ab653bb41fdf5fb88160f43e51b3de49bc8710
3
+ metadata.gz: 6ee4a4ac2c31ebff805ee20edc3658ffe64be32e50b524ee4af3646e3ffc3a3c
4
+ data.tar.gz: 8cbc33a2a696b19c8e614932d1daa7fa9beddaf9d69dd8377d909cb382e40f87
5
5
  SHA512:
6
- metadata.gz: 743edefe6fdaa9ebf9dc2236e4ce672f44d10bcbe954bda7a1560c4166cacf12824eb6c994dfe635543746c402696d981a22224852bb25a7df7558c81bdbbff3
7
- data.tar.gz: 506488a2edac4c9e7b68d09e8b8dd8a970993dc8a87ba7d6bf3200856f4384ebe90362ea2fe337e2cdb4f43cb951b7c79c2a9a26154ed278b1704c369b88e85a
6
+ metadata.gz: ff2ca466abbd198b3bc76c8854113d90033fb606e9f11152ecf6d079564ee4dcbdab359a5b17229770a7dc531a9674b211d079a2204596efe3ec5b67157bf82e
7
+ data.tar.gz: a6a16c64b0ada5c4d1a740894df09a9f41ed0110b06cfd5629b1971c64836a5fd38bceebb7898432b275cb677779606ecd780b917d4095eb161987d26c0eecc0
data/Dockerfile CHANGED
@@ -3,6 +3,8 @@ FROM ruby:2.7.4-alpine
3
3
  RUN apk update --no-cache \
4
4
  && apk add build-base git openssh-client
5
5
 
6
+ COPY . .
7
+
6
8
  RUN gem install hetzner-k3s
7
9
 
8
10
  COPY entrypoint.sh /entrypoint.sh
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hetzner-k3s (0.3.6)
4
+ hetzner-k3s (0.4.0)
5
5
  bcrypt_pbkdf
6
6
  ed25519
7
7
  http
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", firewall_config).body
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
- "description": "Allow port 22 (SSH)",
48
- "direction": "in",
49
- "protocol": "tcp",
50
- "port": "22",
51
- "source_ips": [
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
- "description": "Allow all UDP traffic between nodes on the private network",
91
- "direction": "in",
92
- "protocol": "udp",
93
- "port": "any",
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", load_balancer_config).body
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 load_balancer_config
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
@@ -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
- token = configuration.dig("hetzner_token")
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([A-Za-z0-9\-\_]+)\Z/
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
- false
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
@@ -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
- # Deleting nodes defined according to Kubernetes first
154
- begin
155
- Timeout::timeout(5) do
156
- servers = kubernetes_client.api("v1").resource("nodes").list
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
- Hetzner::LoadBalancer.new(
201
- hetzner_client: hetzner_client,
202
- cluster_name: cluster_name
203
- ).delete(ha: (masters.size > 1))
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
@@ -1,5 +1,5 @@
1
1
  module Hetzner
2
2
  module K3s
3
- VERSION = "0.3.6"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hetzner-k3s
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.6
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-18 00:00:00.000000000 Z
11
+ date: 2021-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor