hetzner-k3s 0.5.0 → 0.5.1

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