hetzner-k3s 0.5.0 → 0.5.4

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