hetzner-k3s 0.4.9 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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