hetzner-k3s 0.4.9 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,20 @@
1
- require 'thread'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'net/ssh'
3
- require "securerandom"
4
- require "base64"
4
+ require 'securerandom'
5
+ require 'base64'
5
6
  require 'timeout'
6
- require "subprocess"
7
-
8
- require_relative "../infra/client"
9
- require_relative "../infra/firewall"
10
- require_relative "../infra/network"
11
- require_relative "../infra/ssh_key"
12
- require_relative "../infra/server"
13
- require_relative "../infra/load_balancer"
14
- require_relative "../infra/placement_group"
7
+ require 'subprocess'
15
8
 
16
- require_relative "../utils"
9
+ require_relative '../infra/client'
10
+ require_relative '../infra/firewall'
11
+ require_relative '../infra/network'
12
+ require_relative '../infra/ssh_key'
13
+ require_relative '../infra/server'
14
+ require_relative '../infra/load_balancer'
15
+ require_relative '../infra/placement_group'
17
16
 
17
+ require_relative '../utils'
18
18
 
19
19
  class Cluster
20
20
  include Utils
@@ -26,18 +26,19 @@ class Cluster
26
26
 
27
27
  def create(configuration:)
28
28
  @configuration = configuration
29
- @cluster_name = configuration.dig("cluster_name")
30
- @kubeconfig_path = File.expand_path(configuration.dig("kubeconfig_path"))
31
- @public_ssh_key_path = File.expand_path(configuration.dig("public_ssh_key_path"))
32
- private_ssh_key_path = configuration.dig("private_ssh_key_path")
33
- @private_ssh_key_path = File.expand_path(private_ssh_key_path) if private_ssh_key_path
34
- @k3s_version = configuration.dig("k3s_version")
35
- @masters_config = configuration.dig("masters")
29
+ @cluster_name = configuration['cluster_name']
30
+ @kubeconfig_path = File.expand_path(configuration['kubeconfig_path'])
31
+ @public_ssh_key_path = File.expand_path(configuration['public_ssh_key_path'])
32
+ private_ssh_key_path = configuration['private_ssh_key_path']
33
+ @private_ssh_key_path = private_ssh_key_path && File.expand_path(private_ssh_key_path)
34
+ @k3s_version = configuration['k3s_version']
35
+ @masters_config = configuration['masters']
36
36
  @worker_node_pools = find_worker_node_pools(configuration)
37
- @location = configuration.dig("location")
38
- @verify_host_key = configuration.fetch("verify_host_key", false)
37
+ @location = configuration['location']
38
+ @verify_host_key = configuration.fetch('verify_host_key', false)
39
39
  @servers = []
40
- @networks = configuration.dig("ssh_allowed_networks")
40
+ @networks = configuration['ssh_allowed_networks']
41
+ @enable_encryption = configuration.fetch('enable_encryption', false)
41
42
 
42
43
  create_resources
43
44
 
@@ -51,17 +52,20 @@ class Cluster
51
52
  end
52
53
 
53
54
  def delete(configuration:)
54
- @cluster_name = configuration.dig("cluster_name")
55
- @kubeconfig_path = File.expand_path(configuration.dig("kubeconfig_path"))
56
- @public_ssh_key_path = File.expand_path(configuration.dig("public_ssh_key_path"))
55
+ @configuration = configuration
56
+ @cluster_name = configuration['cluster_name']
57
+ @kubeconfig_path = File.expand_path(configuration['kubeconfig_path'])
58
+ @public_ssh_key_path = File.expand_path(configuration['public_ssh_key_path'])
59
+ @masters_config = configuration['masters']
60
+ @worker_node_pools = find_worker_node_pools(configuration)
57
61
 
58
62
  delete_resources
59
63
  end
60
64
 
61
65
  def upgrade(configuration:, new_k3s_version:, config_file:)
62
66
  @configuration = configuration
63
- @cluster_name = configuration.dig("cluster_name")
64
- @kubeconfig_path = File.expand_path(configuration.dig("kubeconfig_path"))
67
+ @cluster_name = configuration['cluster_name']
68
+ @kubeconfig_path = File.expand_path(configuration['kubeconfig_path'])
65
69
  @new_k3s_version = new_k3s_version
66
70
  @config_file = config_file
67
71
 
@@ -70,463 +74,510 @@ class Cluster
70
74
 
71
75
  private
72
76
 
73
- def find_worker_node_pools(configuration)
74
- configuration.fetch("worker_node_pools", [])
75
- end
77
+ attr_accessor :servers
76
78
 
77
- attr_accessor :servers
79
+ attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
80
+ :masters_config, :worker_node_pools,
81
+ :location, :public_ssh_key_path,
82
+ :hetzner_token, :new_k3s_version, :configuration,
83
+ :config_file, :verify_host_key, :networks, :private_ssh_key_path,
84
+ :enable_encryption
78
85
 
79
- attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
80
- :masters_config, :worker_node_pools,
81
- :location, :public_ssh_key_path,
82
- :hetzner_token, :tls_sans, :new_k3s_version, :configuration,
83
- :config_file, :verify_host_key, :networks, :private_ssh_key_path, :configuration
86
+ def find_worker_node_pools(configuration)
87
+ configuration.fetch('worker_node_pools', [])
88
+ end
84
89
 
90
+ def latest_k3s_version
91
+ response = HTTP.get('https://api.github.com/repos/k3s-io/k3s/tags').body
92
+ JSON.parse(response).first['name']
93
+ end
85
94
 
86
- def latest_k3s_version
87
- response = HTTP.get("https://api.github.com/repos/k3s-io/k3s/tags").body
88
- JSON.parse(response).first["name"]
95
+ def create_resources
96
+ create_servers
97
+ create_load_balancer if masters.size > 1
98
+ end
99
+
100
+ def delete_placement_groups
101
+ Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:).delete
102
+
103
+ worker_node_pools.each do |pool|
104
+ pool_name = pool['name']
105
+ Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:, pool_name:).delete
89
106
  end
107
+ end
90
108
 
91
- def create_resources
92
- master_instance_type = masters_config["instance_type"]
93
- masters_count = masters_config["instance_count"]
94
-
95
- placement_group_id = Hetzner::PlacementGroup.new(
96
- hetzner_client: hetzner_client,
97
- cluster_name: cluster_name
98
- ).create
99
-
100
- firewall_id = Hetzner::Firewall.new(
101
- hetzner_client: hetzner_client,
102
- cluster_name: cluster_name
103
- ).create(ha: (masters_count > 1), networks: networks)
104
-
105
- network_id = Hetzner::Network.new(
106
- hetzner_client: hetzner_client,
107
- cluster_name: cluster_name
108
- ).create(location: location)
109
-
110
- ssh_key_id = Hetzner::SSHKey.new(
111
- hetzner_client: hetzner_client,
112
- cluster_name: cluster_name
113
- ).create(public_ssh_key_path: public_ssh_key_path)
114
-
115
- server_configs = []
116
-
117
- masters_count.times do |i|
118
- server_configs << {
119
- location: location,
120
- instance_type: master_instance_type,
121
- instance_id: "master#{i+1}",
122
- firewall_id: firewall_id,
123
- network_id: network_id,
124
- ssh_key_id: ssh_key_id,
125
- placement_group_id: placement_group_id,
126
- image: image
127
- }
128
- end
109
+ def delete_resources
110
+ Hetzner::LoadBalancer.new(hetzner_client:, cluster_name:).delete(high_availability: (masters.size > 1))
129
111
 
130
- if masters_count > 1
131
- Hetzner::LoadBalancer.new(
132
- hetzner_client: hetzner_client,
133
- cluster_name: cluster_name
134
- ).create(location: location, network_id: network_id)
135
- end
112
+ Hetzner::Firewall.new(hetzner_client:, cluster_name:).delete(all_servers)
136
113
 
137
- worker_node_pools.each do |worker_node_pool|
138
- worker_node_pool_name = worker_node_pool["name"]
139
- worker_instance_type = worker_node_pool["instance_type"]
140
- worker_count = worker_node_pool["instance_count"]
141
-
142
- worker_count.times do |i|
143
- server_configs << {
144
- location: location,
145
- instance_type: worker_instance_type,
146
- instance_id: "pool-#{worker_node_pool_name}-worker#{i+1}",
147
- firewall_id: firewall_id,
148
- network_id: network_id,
149
- ssh_key_id: ssh_key_id,
150
- placement_group_id: placement_group_id,
151
- image: image
152
- }
153
- end
154
- end
114
+ Hetzner::Network.new(hetzner_client:, cluster_name:).delete
155
115
 
156
- threads = server_configs.map do |server_config|
157
- Thread.new do
158
- servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(**server_config)
159
- end
160
- end
116
+ Hetzner::SSHKey.new(hetzner_client:, cluster_name:).delete(public_ssh_key_path:)
161
117
 
162
- threads.each(&:join) unless threads.empty?
118
+ delete_placement_groups
119
+ delete_servers
120
+ end
163
121
 
164
- while servers.size != server_configs.size
165
- sleep 1
166
- end
122
+ def upgrade_cluster
123
+ worker_upgrade_concurrency = workers.size - 1
124
+ worker_upgrade_concurrency = 1 if worker_upgrade_concurrency.zero?
125
+
126
+ cmd = <<~BASH
127
+ kubectl apply -f - <<-EOF
128
+ apiVersion: upgrade.cattle.io/v1
129
+ kind: Plan
130
+ metadata:
131
+ name: k3s-server
132
+ namespace: system-upgrade
133
+ labels:
134
+ k3s-upgrade: server
135
+ spec:
136
+ concurrency: 1
137
+ version: #{new_k3s_version}
138
+ nodeSelector:
139
+ matchExpressions:
140
+ - {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
141
+ serviceAccountName: system-upgrade
142
+ tolerations:
143
+ - key: "CriticalAddonsOnly"
144
+ operator: "Equal"
145
+ value: "true"
146
+ effect: "NoExecute"
147
+ cordon: true
148
+ upgrade:
149
+ image: rancher/k3s-upgrade
150
+ EOF
151
+ BASH
152
+
153
+ run cmd, kubeconfig_path: kubeconfig_path
154
+
155
+ cmd = <<~BASH
156
+ kubectl apply -f - <<-EOF
157
+ apiVersion: upgrade.cattle.io/v1
158
+ kind: Plan
159
+ metadata:
160
+ name: k3s-agent
161
+ namespace: system-upgrade
162
+ labels:
163
+ k3s-upgrade: agent
164
+ spec:
165
+ concurrency: #{worker_upgrade_concurrency}
166
+ version: #{new_k3s_version}
167
+ nodeSelector:
168
+ matchExpressions:
169
+ - {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
170
+ serviceAccountName: system-upgrade
171
+ prepare:
172
+ image: rancher/k3s-upgrade
173
+ args: ["prepare", "k3s-server"]
174
+ cordon: true
175
+ upgrade:
176
+ image: rancher/k3s-upgrade
177
+ EOF
178
+ BASH
167
179
 
168
- puts
169
- threads = servers.map do |server|
170
- Thread.new { wait_for_ssh server }
171
- end
180
+ run cmd, kubeconfig_path: kubeconfig_path
172
181
 
173
- threads.each(&:join) unless threads.empty?
174
- end
182
+ 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.'
183
+ puts 'The API server may be briefly unavailable during the upgrade of the controlplane.'
184
+
185
+ configuration['k3s_version'] = new_k3s_version
186
+
187
+ File.write(config_file, configuration.to_yaml)
188
+ end
189
+
190
+ def master_script(master)
191
+ server = master == first_master ? ' --cluster-init ' : " --server https://#{api_server_ip}:6443 "
192
+ flannel_interface = find_flannel_interface(master)
193
+ flannel_wireguard = enable_encryption ? ' --flannel-backend=wireguard ' : ' '
194
+
195
+ taint = schedule_workloads_on_masters? ? ' ' : ' --node-taint CriticalAddonsOnly=true:NoExecute '
196
+
197
+ <<~SCRIPT
198
+ curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
199
+ --disable-cloud-controller \
200
+ --disable servicelb \
201
+ --disable traefik \
202
+ --disable local-storage \
203
+ --disable metrics-server \
204
+ --write-kubeconfig-mode=644 \
205
+ --node-name="$(hostname -f)" \
206
+ --cluster-cidr=10.244.0.0/16 \
207
+ --etcd-expose-metrics=true \
208
+ #{flannel_wireguard} \
209
+ --kube-controller-manager-arg="address=0.0.0.0" \
210
+ --kube-controller-manager-arg="bind-address=0.0.0.0" \
211
+ --kube-proxy-arg="metrics-bind-address=0.0.0.0" \
212
+ --kube-scheduler-arg="address=0.0.0.0" \
213
+ --kube-scheduler-arg="bind-address=0.0.0.0" \
214
+ #{taint} \
215
+ --kubelet-arg="cloud-provider=external" \
216
+ --advertise-address=$(hostname -I | awk '{print $2}') \
217
+ --node-ip=$(hostname -I | awk '{print $2}') \
218
+ --node-external-ip=$(hostname -I | awk '{print $1}') \
219
+ --flannel-iface=#{flannel_interface} \
220
+ #{server} #{tls_sans}" sh -
221
+ SCRIPT
222
+ end
223
+
224
+ def worker_script(worker)
225
+ flannel_interface = find_flannel_interface(worker)
226
+
227
+ <<~BASH
228
+ curl -sfL https://get.k3s.io | K3S_TOKEN="#{k3s_token}" INSTALL_K3S_VERSION="#{k3s_version}" K3S_URL=https://#{first_master_private_ip}:6443 INSTALL_K3S_EXEC="agent \
229
+ --node-name="$(hostname -f)" \
230
+ --kubelet-arg="cloud-provider=external" \
231
+ --node-ip=$(hostname -I | awk '{print $2}') \
232
+ --node-external-ip=$(hostname -I | awk '{print $1}') \
233
+ --flannel-iface=#{flannel_interface}" sh -
234
+ BASH
235
+ end
236
+
237
+ def deploy_kubernetes
238
+ puts
239
+ puts "Deploying k3s to first master (#{first_master['name']})..."
175
240
 
176
- def delete_resources
177
- Hetzner::PlacementGroup.new(
178
- hetzner_client: hetzner_client,
179
- cluster_name: cluster_name
180
- ).delete
181
-
182
- Hetzner::LoadBalancer.new(
183
- hetzner_client: hetzner_client,
184
- cluster_name: cluster_name
185
- ).delete(ha: (masters.size > 1))
186
-
187
- Hetzner::Firewall.new(
188
- hetzner_client: hetzner_client,
189
- cluster_name: cluster_name
190
- ).delete(all_servers)
191
-
192
- Hetzner::Network.new(
193
- hetzner_client: hetzner_client,
194
- cluster_name: cluster_name
195
- ).delete
196
-
197
- Hetzner::SSHKey.new(
198
- hetzner_client: hetzner_client,
199
- cluster_name: cluster_name
200
- ).delete(public_ssh_key_path: public_ssh_key_path)
201
-
202
- threads = all_servers.map do |server|
241
+ ssh first_master, master_script(first_master), print_output: true
242
+
243
+ puts
244
+ puts '...k3s has been deployed to first master.'
245
+
246
+ save_kubeconfig
247
+
248
+ if masters.size > 1
249
+ threads = masters[1..].map do |master|
203
250
  Thread.new do
204
- Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).delete(server_name: server["name"])
251
+ puts
252
+ puts "Deploying k3s to master #{master['name']}..."
253
+
254
+ ssh master, master_script(master), print_output: true
255
+
256
+ puts
257
+ puts "...k3s has been deployed to master #{master['name']}."
205
258
  end
206
259
  end
207
260
 
208
261
  threads.each(&:join) unless threads.empty?
209
262
  end
210
263
 
211
- def upgrade_cluster
212
- worker_upgrade_concurrency = workers.size - 1
213
- worker_upgrade_concurrency = 1 if worker_upgrade_concurrency == 0
214
-
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)
277
- end
264
+ threads = workers.map do |worker|
265
+ Thread.new do
266
+ puts
267
+ puts "Deploying k3s to worker (#{worker['name']})..."
278
268
 
269
+ ssh worker, worker_script(worker), print_output: true
279
270
 
280
- def master_script(master)
281
- server = master == first_master ? " --cluster-init " : " --server https://#{first_master_private_ip}:6443 "
282
- flannel_interface = find_flannel_interface(master)
283
-
284
- taint = schedule_workloads_on_masters? ? " " : " --node-taint CriticalAddonsOnly=true:NoExecute "
285
-
286
- <<~EOF
287
- curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="#{k3s_version}" K3S_TOKEN="#{k3s_token}" INSTALL_K3S_EXEC="server \
288
- --disable-cloud-controller \
289
- --disable servicelb \
290
- --disable traefik \
291
- --disable local-storage \
292
- --disable metrics-server \
293
- --write-kubeconfig-mode=644 \
294
- --node-name="$(hostname -f)" \
295
- --cluster-cidr=10.244.0.0/16 \
296
- --etcd-expose-metrics=true \
297
- --kube-controller-manager-arg="address=0.0.0.0" \
298
- --kube-controller-manager-arg="bind-address=0.0.0.0" \
299
- --kube-proxy-arg="metrics-bind-address=0.0.0.0" \
300
- --kube-scheduler-arg="address=0.0.0.0" \
301
- --kube-scheduler-arg="bind-address=0.0.0.0" \
302
- #{taint} \
303
- --kubelet-arg="cloud-provider=external" \
304
- --advertise-address=$(hostname -I | awk '{print $2}') \
305
- --node-ip=$(hostname -I | awk '{print $2}') \
306
- --node-external-ip=$(hostname -I | awk '{print $1}') \
307
- --flannel-iface=#{flannel_interface} \
308
- #{server} #{tls_sans}" sh -
309
- EOF
271
+ puts
272
+ puts "...k3s has been deployed to worker (#{worker['name']})."
273
+ end
310
274
  end
311
275
 
312
- def worker_script(worker)
313
- flannel_interface = find_flannel_interface(worker)
276
+ threads.each(&:join) unless threads.empty?
277
+ end
314
278
 
315
- <<~EOF
316
- curl -sfL https://get.k3s.io | K3S_TOKEN="#{k3s_token}" INSTALL_K3S_VERSION="#{k3s_version}" K3S_URL=https://#{first_master_private_ip}:6443 INSTALL_K3S_EXEC="agent \
317
- --node-name="$(hostname -f)" \
318
- --kubelet-arg="cloud-provider=external" \
319
- --node-ip=$(hostname -I | awk '{print $2}') \
320
- --node-external-ip=$(hostname -I | awk '{print $1}') \
321
- --flannel-iface=#{flannel_interface}" sh -
279
+ def deploy_cloud_controller_manager
280
+ check_kubectl
281
+
282
+ puts
283
+ puts 'Deploying Hetzner Cloud Controller Manager...'
284
+
285
+ cmd = <<~BASH
286
+ kubectl apply -f - <<-EOF
287
+ apiVersion: "v1"
288
+ kind: "Secret"
289
+ metadata:
290
+ namespace: 'kube-system'
291
+ name: 'hcloud'
292
+ stringData:
293
+ network: "#{cluster_name}"
294
+ token: "#{hetzner_token}"
322
295
  EOF
323
- end
296
+ BASH
324
297
 
325
- def deploy_kubernetes
326
- puts
327
- puts "Deploying k3s to first master (#{first_master["name"]})..."
298
+ run cmd, kubeconfig_path: kubeconfig_path
328
299
 
329
- ssh first_master, master_script(first_master), print_output: true
300
+ cmd = 'kubectl apply -f https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml'
330
301
 
331
- puts
332
- puts "...k3s has been deployed to first master."
302
+ run cmd, kubeconfig_path: kubeconfig_path
333
303
 
334
- save_kubeconfig
304
+ puts '...Cloud Controller Manager deployed'
305
+ end
335
306
 
336
- if masters.size > 1
337
- threads = masters[1..-1].map do |master|
338
- Thread.new do
339
- puts
340
- puts "Deploying k3s to master #{master["name"]}..."
307
+ def deploy_system_upgrade_controller
308
+ check_kubectl
341
309
 
342
- ssh master, master_script(master), print_output: true
310
+ puts
311
+ puts 'Deploying k3s System Upgrade Controller...'
343
312
 
344
- puts
345
- puts "...k3s has been deployed to master #{master["name"]}."
346
- end
347
- end
313
+ cmd = 'kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.1/system-upgrade-controller.yaml'
348
314
 
349
- threads.each(&:join) unless threads.empty?
350
- end
315
+ run cmd, kubeconfig_path: kubeconfig_path
351
316
 
352
- threads = workers.map do |worker|
353
- Thread.new do
354
- puts
355
- puts "Deploying k3s to worker (#{worker["name"]})..."
317
+ puts '...k3s System Upgrade Controller deployed'
318
+ end
356
319
 
357
- ssh worker, worker_script(worker), print_output: true
320
+ def deploy_csi_driver
321
+ check_kubectl
322
+
323
+ puts
324
+ puts 'Deploying Hetzner CSI Driver...'
325
+
326
+ cmd = <<~BASH
327
+ kubectl apply -f - <<-EOF
328
+ apiVersion: "v1"
329
+ kind: "Secret"
330
+ metadata:
331
+ namespace: 'kube-system'
332
+ name: 'hcloud-csi'
333
+ stringData:
334
+ token: "#{hetzner_token}"
335
+ EOF
336
+ BASH
358
337
 
359
- puts
360
- puts "...k3s has been deployed to worker (#{worker["name"]})."
361
- end
362
- end
338
+ run cmd, kubeconfig_path: kubeconfig_path
363
339
 
364
- threads.each(&:join) unless threads.empty?
365
- end
340
+ cmd = 'kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml'
341
+
342
+ run cmd, kubeconfig_path: kubeconfig_path
366
343
 
367
- def deploy_cloud_controller_manager
368
- check_kubectl
344
+ puts '...CSI Driver deployed'
345
+ end
369
346
 
370
- puts
371
- puts "Deploying Hetzner Cloud Controller Manager..."
347
+ def find_flannel_interface(server)
348
+ if ssh(server, 'lscpu | grep Vendor') =~ /Intel/
349
+ 'ens10'
350
+ else
351
+ 'enp7s0'
352
+ end
353
+ end
372
354
 
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
355
+ def all_servers
356
+ @all_servers ||= hetzner_client.get('/servers?sort=created:desc')['servers'].select do |server|
357
+ belongs_to_cluster?(server) == true
358
+ end
359
+ end
385
360
 
386
- run cmd, kubeconfig_path: kubeconfig_path
361
+ def masters
362
+ @masters ||= all_servers.select { |server| server['name'] =~ /master\d+\Z/ }.sort { |a, b| a['name'] <=> b['name'] }
363
+ end
387
364
 
388
- cmd = "kubectl apply -f https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml"
365
+ def workers
366
+ @workers = all_servers.select { |server| server['name'] =~ /worker\d+\Z/ }.sort { |a, b| a['name'] <=> b['name'] }
367
+ end
389
368
 
390
- run cmd, kubeconfig_path: kubeconfig_path
369
+ def k3s_token
370
+ @k3s_token ||= begin
371
+ token = ssh(first_master, '{ TOKEN=$(< /var/lib/rancher/k3s/server/node-token); } 2> /dev/null; echo $TOKEN')
391
372
 
392
- puts "...Cloud Controller Manager deployed"
373
+ if token.empty?
374
+ SecureRandom.hex
375
+ else
376
+ token.split(':').last
377
+ end
393
378
  end
379
+ end
394
380
 
395
- def deploy_system_upgrade_controller
396
- check_kubectl
381
+ def first_master_private_ip
382
+ @first_master_private_ip ||= first_master['private_net'][0]['ip']
383
+ end
397
384
 
398
- puts
399
- puts "Deploying k3s System Upgrade Controller..."
385
+ def first_master
386
+ masters.first
387
+ end
400
388
 
401
- cmd = "kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.1/system-upgrade-controller.yaml"
389
+ def api_server_ip
390
+ return @api_server_ip if @api_server_ip
391
+
392
+ @api_server_ip = if masters.size > 1
393
+ load_balancer_name = "#{cluster_name}-api"
394
+ load_balancer = hetzner_client.get('/load_balancers')['load_balancers'].detect do |lb|
395
+ lb['name'] == load_balancer_name
396
+ end
397
+ load_balancer['public_net']['ipv4']['ip']
398
+ else
399
+ first_master_public_ip
400
+ end
401
+ end
402
402
 
403
- run cmd, kubeconfig_path: kubeconfig_path
403
+ def tls_sans
404
+ sans = " --tls-san=#{api_server_ip} "
404
405
 
405
- puts "...k3s System Upgrade Controller deployed"
406
+ masters.each do |master|
407
+ master_private_ip = master['private_net'][0]['ip']
408
+ sans << " --tls-san=#{master_private_ip} "
406
409
  end
407
410
 
408
- def deploy_csi_driver
409
- check_kubectl
411
+ sans
412
+ end
410
413
 
411
- puts
412
- puts "Deploying Hetzner CSI Driver..."
414
+ def first_master_public_ip
415
+ @first_master_public_ip ||= first_master.dig('public_net', 'ipv4', 'ip')
416
+ end
413
417
 
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
418
+ def save_kubeconfig
419
+ kubeconfig = ssh(first_master, 'cat /etc/rancher/k3s/k3s.yaml')
420
+ .gsub('127.0.0.1', api_server_ip)
421
+ .gsub('default', cluster_name)
425
422
 
426
- run cmd, kubeconfig_path: kubeconfig_path
423
+ File.write(kubeconfig_path, kubeconfig)
427
424
 
428
- cmd = "kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml"
425
+ FileUtils.chmod 'go-r', kubeconfig_path
426
+ end
429
427
 
430
- run cmd, kubeconfig_path: kubeconfig_path
428
+ def belongs_to_cluster?(server)
429
+ server.dig('labels', 'cluster') == cluster_name
430
+ end
431
431
 
432
- puts "...CSI Driver deployed"
433
- end
432
+ def schedule_workloads_on_masters?
433
+ schedule_workloads_on_masters = configuration['schedule_workloads_on_masters']
434
+ schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
435
+ end
434
436
 
435
- def find_flannel_interface(server)
436
- if ssh(server, "lscpu | grep Vendor") =~ /Intel/
437
- "ens10"
438
- else
439
- "enp7s0"
440
- end
441
- end
437
+ def image
438
+ configuration['image'] || 'ubuntu-20.04'
439
+ end
442
440
 
443
- def all_servers
444
- @all_servers ||= hetzner_client.get("/servers")["servers"].select{ |server| belongs_to_cluster?(server) == true }
445
- end
441
+ def additional_packages
442
+ configuration['additional_packages'] || []
443
+ end
446
444
 
447
- def masters
448
- @masters ||= all_servers.select{ |server| server["name"] =~ /master\d+\Z/ }.sort{ |a, b| a["name"] <=> b["name"] }
449
- end
445
+ def check_kubectl
446
+ return if which('kubectl')
450
447
 
451
- def workers
452
- @workers = all_servers.select{ |server| server["name"] =~ /worker\d+\Z/ }.sort{ |a, b| a["name"] <=> b["name"] }
453
- end
448
+ puts 'Please ensure kubectl is installed and in your PATH.'
449
+ exit 1
450
+ end
454
451
 
455
- def k3s_token
456
- @k3s_token ||= begin
457
- token = ssh(first_master, "{ TOKEN=$(< /var/lib/rancher/k3s/server/node-token); } 2> /dev/null; echo $TOKEN")
452
+ def placement_group_id(pool_name = nil)
453
+ @placement_groups ||= {}
454
+ @placement_groups[pool_name || '__masters__'] ||= Hetzner::PlacementGroup.new(hetzner_client:, cluster_name:, pool_name:).create
455
+ end
458
456
 
459
- if token.empty?
460
- SecureRandom.hex
461
- else
462
- token.split(":").last
463
- end
464
- end
465
- end
457
+ def master_instance_type
458
+ @master_instance_type ||= masters_config['instance_type']
459
+ end
466
460
 
467
- def first_master_private_ip
468
- @first_master_private_ip ||= first_master["private_net"][0]["ip"]
469
- end
461
+ def masters_count
462
+ @masters_count ||= masters_config['instance_count']
463
+ end
470
464
 
471
- def first_master
472
- masters.first
473
- end
465
+ def firewall_id
466
+ @firewall_id ||= Hetzner::Firewall.new(hetzner_client:, cluster_name:).create(high_availability: (masters_count > 1), networks:)
467
+ end
474
468
 
475
- def api_server_ip
476
- return @api_server_ip if @api_server_ip
469
+ def network_id
470
+ @network_id ||= Hetzner::Network.new(hetzner_client:, cluster_name:).create(location:)
471
+ end
477
472
 
478
- @api_server_ip = if masters.size > 1
479
- load_balancer_name = "#{cluster_name}-api"
480
- load_balancer = hetzner_client.get("/load_balancers")["load_balancers"].detect{ |load_balancer| load_balancer["name"] == load_balancer_name }
481
- load_balancer["public_net"]["ipv4"]["ip"]
482
- else
483
- first_master_public_ip
484
- end
473
+ def ssh_key_id
474
+ @ssh_key_id ||= Hetzner::SSHKey.new(hetzner_client:, cluster_name:).create(public_ssh_key_path:)
475
+ end
476
+
477
+ def master_definitions_for_create
478
+ definitions = []
479
+
480
+ masters_count.times do |i|
481
+ definitions << {
482
+ instance_type: master_instance_type,
483
+ instance_id: "master#{i + 1}",
484
+ placement_group_id:,
485
+ location:,
486
+ firewall_id:,
487
+ network_id:,
488
+ ssh_key_id:,
489
+ image:,
490
+ additional_packages:
491
+ }
485
492
  end
486
493
 
487
- def tls_sans
488
- sans = " --tls-san=#{api_server_ip} "
494
+ definitions
495
+ end
489
496
 
490
- masters.each do |master|
491
- master_private_ip = master["private_net"][0]["ip"]
492
- sans << " --tls-san=#{master_private_ip} "
493
- end
497
+ def master_definitions_for_delete
498
+ definitions = []
494
499
 
495
- sans
500
+ masters_count.times do |i|
501
+ definitions << {
502
+ instance_type: master_instance_type,
503
+ instance_id: "master#{i + 1}"
504
+ }
496
505
  end
497
506
 
498
- def first_master_public_ip
499
- @first_master_public_ip ||= first_master.dig("public_net", "ipv4", "ip")
507
+ definitions
508
+ end
509
+
510
+ def worker_node_pool_definitions(worker_node_pool)
511
+ worker_node_pool_name = worker_node_pool['name']
512
+ worker_instance_type = worker_node_pool['instance_type']
513
+ worker_count = worker_node_pool['instance_count']
514
+
515
+ definitions = []
516
+
517
+ worker_count.times do |i|
518
+ definitions << {
519
+ instance_type: worker_instance_type,
520
+ instance_id: "pool-#{worker_node_pool_name}-worker#{i + 1}",
521
+ placement_group_id: placement_group_id(worker_node_pool_name),
522
+ location:,
523
+ firewall_id:,
524
+ network_id:,
525
+ ssh_key_id:,
526
+ image:,
527
+ additional_packages:
528
+ }
500
529
  end
501
530
 
502
- def save_kubeconfig
503
- kubeconfig = ssh(first_master, "cat /etc/rancher/k3s/k3s.yaml").
504
- gsub("127.0.0.1", api_server_ip).
505
- gsub("default", cluster_name)
531
+ definitions
532
+ end
533
+
534
+ def create_load_balancer
535
+ Hetzner::LoadBalancer.new(hetzner_client:, cluster_name:).create(location:, network_id:)
536
+ end
506
537
 
507
- File.write(kubeconfig_path, kubeconfig)
538
+ def server_configs
539
+ return @server_configs if @server_configs
508
540
 
509
- FileUtils.chmod "go-r", kubeconfig_path
510
- end
541
+ @server_configs = master_definitions_for_create
511
542
 
512
- def belongs_to_cluster?(server)
513
- server.dig("labels", "cluster") == cluster_name
543
+ worker_node_pools.each do |worker_node_pool|
544
+ @server_configs += worker_node_pool_definitions(worker_node_pool)
514
545
  end
515
546
 
516
- def schedule_workloads_on_masters?
517
- schedule_workloads_on_masters = configuration.dig("schedule_workloads_on_masters")
518
- schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
547
+ @server_configs
548
+ end
549
+
550
+ def create_servers
551
+ servers = []
552
+
553
+ threads = server_configs.map do |server_config|
554
+ Thread.new do
555
+ servers << Hetzner::Server.new(hetzner_client:, cluster_name:).create(**server_config)
556
+ end
519
557
  end
520
558
 
521
- def image
522
- configuration.dig("image") || "ubuntu-20.04"
559
+ threads.each(&:join) unless threads.empty?
560
+
561
+ sleep 1 while servers.size != server_configs.size
562
+
563
+ wait_for_servers(servers)
564
+ end
565
+
566
+ def wait_for_servers(servers)
567
+ threads = servers.map do |server|
568
+ Thread.new { wait_for_ssh server }
523
569
  end
524
570
 
525
- def check_kubectl
526
- unless which("kubectl")
527
- puts "Please ensure kubectl is installed and in your PATH."
528
- exit 1
571
+ threads.each(&:join) unless threads.empty?
572
+ end
573
+
574
+ def delete_servers
575
+ threads = all_servers.map do |server|
576
+ Thread.new do
577
+ Hetzner::Server.new(hetzner_client:, cluster_name:).delete(server_name: server['name'])
529
578
  end
530
579
  end
531
580
 
581
+ threads.each(&:join) unless threads.empty?
582
+ end
532
583
  end