hetzner-k3s 0.4.9 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,14 @@
1
- require "thor"
2
- require "http"
3
- require "sshkey"
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'http'
5
+ require 'sshkey'
4
6
  require 'ipaddr'
5
7
  require 'open-uri'
6
- require "yaml"
7
-
8
- require_relative "cluster"
9
- require_relative "version"
8
+ require 'yaml'
10
9
 
10
+ require_relative 'cluster'
11
+ require_relative 'version'
11
12
 
12
13
  module Hetzner
13
14
  module K3s
@@ -16,348 +17,364 @@ module Hetzner
16
17
  true
17
18
  end
18
19
 
19
- desc "version", "Print the version"
20
+ def initialize(*args)
21
+ @errors = []
22
+ @used_server_types = []
23
+
24
+ super
25
+ end
26
+
27
+ desc 'version', 'Print the version'
20
28
  def version
21
29
  puts Hetzner::K3s::VERSION
22
30
  end
23
31
 
24
- desc "create-cluster", "Create a k3s cluster in Hetzner Cloud"
32
+ desc 'create-cluster', 'Create a k3s cluster in Hetzner Cloud'
25
33
  option :config_file, required: true
26
-
27
34
  def create_cluster
28
- validate_config_file :create
29
- Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).create configuration: configuration
35
+ validate_configuration :create
36
+ Cluster.new(hetzner_client:, hetzner_token:).create configuration:
30
37
  end
31
38
 
32
- desc "delete-cluster", "Delete an existing k3s cluster in Hetzner Cloud"
39
+ desc 'delete-cluster', 'Delete an existing k3s cluster in Hetzner Cloud'
33
40
  option :config_file, required: true
34
-
35
41
  def delete_cluster
36
- validate_config_file :delete
37
- Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).delete configuration: configuration
42
+ validate_configuration :delete
43
+ Cluster.new(hetzner_client:, hetzner_token:).delete configuration:
38
44
  end
39
45
 
40
- desc "upgrade-cluster", "Upgrade an existing k3s cluster in Hetzner Cloud to a new version"
46
+ desc 'upgrade-cluster', 'Upgrade an existing k3s cluster in Hetzner Cloud to a new version'
41
47
  option :config_file, required: true
42
48
  option :new_k3s_version, required: true
43
- option :force, default: "false"
44
-
49
+ option :force, default: 'false'
45
50
  def upgrade_cluster
46
- validate_config_file :upgrade
47
- Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).upgrade configuration: configuration, new_k3s_version: options[:new_k3s_version], config_file: options[:config_file]
51
+ validate_configuration :upgrade
52
+
53
+ Cluster.new(hetzner_client:, hetzner_token:)
54
+ .upgrade(configuration:, new_k3s_version: options[:new_k3s_version], config_file: options[:config_file])
48
55
  end
49
56
 
50
- desc "releases", "List available k3s releases"
57
+ desc 'releases', 'List available k3s releases'
51
58
  def releases
52
- find_available_releases.each do |release|
59
+ available_releases.each do |release|
53
60
  puts release
54
61
  end
55
62
  end
56
63
 
57
64
  private
58
65
 
59
- attr_reader :configuration, :hetzner_client, :k3s_version
60
- attr_accessor :errors, :used_server_types
61
-
62
- def validate_config_file(action)
63
- config_file_path = options[:config_file]
64
-
65
- if File.exists?(config_file_path)
66
- begin
67
- @configuration = YAML.load_file(options[:config_file])
68
- unless configuration.is_a? Hash
69
- raise "Configuration is invalid"
70
- exit 1
71
- end
72
- rescue => e
73
- puts "Please ensure that the config file is a correct YAML manifest."
74
- exit 1
75
- end
76
- else
77
- puts "Please specify a correct path for the config file."
78
- exit 1
79
- end
66
+ attr_reader :configuration, :hetzner_client, :k3s_version
67
+ attr_accessor :errors, :used_server_types
68
+
69
+ def validate_configuration(action)
70
+ validate_configuration_file
71
+ validate_token
72
+ validate_cluster_name
73
+ validate_kubeconfig_path
74
+
75
+ case action
76
+ when :create
77
+ validate_create
78
+ when :delete
79
+ validate_kubeconfig_path_must_exist
80
+ when :upgrade
81
+ validate_upgrade
82
+ end
80
83
 
81
- @errors = []
82
- @used_server_types = []
83
-
84
- validate_token
85
- validate_cluster_name
86
- validate_kubeconfig_path
87
-
88
- case action
89
- when :create
90
- validate_public_ssh_key
91
- validate_private_ssh_key
92
- validate_ssh_allowed_networks
93
- validate_location
94
- validate_k3s_version
95
- validate_masters
96
- validate_worker_node_pools
97
- validate_verify_host_key
98
- when :delete
99
- validate_kubeconfig_path_must_exist
100
- when :upgrade
101
- validate_kubeconfig_path_must_exist
102
- validate_new_k3s_version
103
- end
84
+ errors.flatten!
104
85
 
105
- errors.flatten!
86
+ return if errors.empty?
106
87
 
107
- unless errors.empty?
108
- puts "Some information in the configuration file requires your attention:"
109
- errors.each do |error|
110
- puts " - #{error}"
111
- end
88
+ puts 'Some information in the configuration file requires your attention:'
112
89
 
113
- exit 1
114
- end
90
+ errors.each do |error|
91
+ puts " - #{error}"
115
92
  end
116
93
 
117
- def valid_token?
118
- return @valid unless @valid.nil?
94
+ exit 1
95
+ end
119
96
 
120
- begin
121
- token = find_hetzner_token
122
- @hetzner_client = Hetzner::Client.new(token: token)
123
- response = hetzner_client.get("/locations")
124
- error_code = response.dig("error", "code")
125
- @valid = if error_code and error_code.size > 0
126
- false
127
- else
128
- true
129
- end
130
- rescue
131
- @valid = false
132
- end
97
+ def valid_token?
98
+ return @valid unless @valid.nil?
99
+
100
+ begin
101
+ token = hetzner_token
102
+ @hetzner_client = Hetzner::Client.new(token:)
103
+ response = hetzner_client.get('/locations')
104
+ error_code = response.dig('error', 'code')
105
+ @valid = error_code&.size != 0
106
+ rescue StandardError
107
+ @valid = false
133
108
  end
109
+ end
134
110
 
135
- def validate_token
136
- errors << "Invalid Hetzner Cloud token" unless valid_token?
137
- end
111
+ def validate_token
112
+ errors << 'Invalid Hetzner Cloud token' unless valid_token?
113
+ end
138
114
 
139
- def validate_cluster_name
140
- errors << "Cluster name is an invalid format (only lowercase letters, digits and dashes are allowed)" unless configuration["cluster_name"] =~ /\A[a-z\d-]+\z/
141
- errors << "Ensure that the cluster name starts with a normal letter" unless configuration["cluster_name"] =~ /\A[a-z]+.*\z/
142
- end
115
+ def validate_cluster_name
116
+ errors << 'Cluster name is an invalid format (only lowercase letters, digits and dashes are allowed)' unless configuration['cluster_name'] =~ /\A[a-z\d-]+\z/
143
117
 
144
- def validate_kubeconfig_path
145
- path = File.expand_path(configuration.dig("kubeconfig_path"))
146
- errors << "kubeconfig path cannot be a directory" and return if File.directory? path
118
+ return if configuration['cluster_name'] =~ /\A[a-z]+.*\z/
147
119
 
148
- directory = File.dirname(path)
149
- errors << "Directory #{directory} doesn't exist" unless File.exists? directory
150
- rescue
151
- errors << "Invalid path for the kubeconfig"
152
- end
120
+ errors << 'Ensure that the cluster name starts with a normal letter'
121
+ end
153
122
 
154
- def validate_public_ssh_key
155
- path = File.expand_path(configuration.dig("public_ssh_key_path"))
156
- errors << "Invalid Public SSH key path" and return unless File.exists? path
123
+ def validate_kubeconfig_path
124
+ path = File.expand_path(configuration['kubeconfig_path'])
125
+ errors << 'kubeconfig path cannot be a directory' and return if File.directory? path
157
126
 
158
- key = File.read(path)
159
- errors << "Public SSH key is invalid" unless ::SSHKey.valid_ssh_public_key?(key)
160
- rescue
161
- errors << "Invalid Public SSH key path"
162
- end
127
+ directory = File.dirname(path)
128
+ errors << "Directory #{directory} doesn't exist" unless File.exist? directory
129
+ rescue StandardError
130
+ errors << 'Invalid path for the kubeconfig'
131
+ end
163
132
 
164
- def validate_private_ssh_key
165
- return unless (private_ssh_key_path = configuration.dig("private_ssh_key_path"))
133
+ def validate_public_ssh_key
134
+ path = File.expand_path(configuration['public_ssh_key_path'])
135
+ errors << 'Invalid Public SSH key path' and return unless File.exist? path
166
136
 
167
- path = File.expand_path(private_ssh_key_path)
168
- errors << "Invalid Private SSH key path" and return unless File.exists?(path)
169
- rescue
170
- errors << "Invalid Private SSH key path"
171
- end
137
+ key = File.read(path)
138
+ errors << 'Public SSH key is invalid' unless ::SSHKey.valid_ssh_public_key?(key)
139
+ rescue StandardError
140
+ errors << 'Invalid Public SSH key path'
141
+ end
172
142
 
173
- def validate_kubeconfig_path_must_exist
174
- path = File.expand_path configuration.dig("kubeconfig_path")
175
- errors << "kubeconfig path is invalid" and return unless File.exists? path
176
- errors << "kubeconfig path cannot be a directory" if File.directory? path
177
- rescue
178
- errors << "Invalid kubeconfig path"
179
- end
143
+ def validate_private_ssh_key
144
+ private_ssh_key_path = configuration['private_ssh_key_path']
180
145
 
181
- def server_types
182
- return [] unless valid_token?
183
- @server_types ||= hetzner_client.get("/server_types")["server_types"].map{ |server_type| server_type["name"] }
184
- rescue
185
- @errors << "Cannot fetch server types with Hetzner API, please try again later"
186
- false
187
- end
146
+ return unless private_ssh_key_path
188
147
 
189
- def locations
190
- return [] unless valid_token?
191
- @locations ||= hetzner_client.get("/locations")["locations"].map{ |location| location["name"] }
192
- rescue
193
- @errors << "Cannot fetch locations with Hetzner API, please try again later"
194
- []
195
- end
148
+ path = File.expand_path(private_ssh_key_path)
149
+ errors << 'Invalid Private SSH key path' and return unless File.exist?(path)
150
+ rescue StandardError
151
+ errors << 'Invalid Private SSH key path'
152
+ end
196
153
 
197
- def validate_location
198
- return if locations.empty? && !valid_token?
199
- errors << "Invalid location - available locations: nbg1 (Nuremberg, Germany), fsn1 (Falkenstein, Germany), hel1 (Helsinki, Finland) or ash (Ashburn, Virginia, USA)" unless locations.include? configuration.dig("location")
200
- end
154
+ def validate_kubeconfig_path_must_exist
155
+ path = File.expand_path configuration['kubeconfig_path']
156
+ errors << 'kubeconfig path is invalid' and return unless File.exist? path
201
157
 
202
- def find_available_releases
203
- @available_releases ||= begin
204
- response = HTTP.get("https://api.github.com/repos/k3s-io/k3s/tags?per_page=999").body
205
- JSON.parse(response).map { |hash| hash["name"] }
206
- end
207
- rescue
208
- errors << "Cannot fetch the releases with Hetzner API, please try again later"
209
- end
158
+ errors << 'kubeconfig path cannot be a directory' if File.directory? path
159
+ rescue StandardError
160
+ errors << 'Invalid kubeconfig path'
161
+ end
210
162
 
211
- def validate_k3s_version
212
- k3s_version = configuration.dig("k3s_version")
213
- available_releases = find_available_releases
214
- errors << "Invalid k3s version" unless available_releases.include? k3s_version
215
- end
163
+ def server_types
164
+ return [] unless valid_token?
216
165
 
217
- def validate_new_k3s_version
218
- new_k3s_version = options[:new_k3s_version]
219
- available_releases = find_available_releases
220
- errors << "The new k3s version is invalid" unless available_releases.include? new_k3s_version
221
- end
166
+ @server_types ||= hetzner_client.get('/server_types')['server_types'].map { |server_type| server_type['name'] }
167
+ rescue StandardError
168
+ @errors << 'Cannot fetch server types with Hetzner API, please try again later'
169
+ false
170
+ end
222
171
 
223
- def validate_masters
224
- masters_pool = nil
172
+ def locations
173
+ return [] unless valid_token?
225
174
 
226
- begin
227
- masters_pool = configuration.dig("masters")
228
- rescue
229
- errors << "Invalid masters configuration"
230
- return
231
- end
175
+ @locations ||= hetzner_client.get('/locations')['locations'].map { |location| location['name'] }
176
+ rescue StandardError
177
+ @errors << 'Cannot fetch locations with Hetzner API, please try again later'
178
+ []
179
+ end
232
180
 
233
- if masters_pool.nil?
234
- errors << "Invalid masters configuration"
235
- return
236
- end
181
+ def validate_location
182
+ return if locations.empty? && !valid_token?
183
+ return if locations.include? configuration['location']
237
184
 
238
- validate_instance_group masters_pool, workers: false
185
+ errors << 'Invalid location - available locations: nbg1 (Nuremberg, Germany), fsn1 (Falkenstein, Germany), hel1 (Helsinki, Finland) or ash (Ashburn, Virginia, USA)'
186
+ end
187
+
188
+ def available_releases
189
+ @available_releases ||= begin
190
+ response = HTTP.get('https://api.github.com/repos/k3s-io/k3s/tags?per_page=999').body
191
+ JSON.parse(response).map { |hash| hash['name'] }
239
192
  end
193
+ rescue StandardError
194
+ errors << 'Cannot fetch the releases with Hetzner API, please try again later'
195
+ end
240
196
 
241
- def validate_worker_node_pools
242
- worker_node_pools = nil
197
+ def validate_k3s_version
198
+ k3s_version = configuration['k3s_version']
199
+ errors << 'Invalid k3s version' unless available_releases.include? k3s_version
200
+ end
243
201
 
244
- begin
245
- worker_node_pools = configuration.dig("worker_node_pools")
246
- rescue
247
- unless schedule_workloads_on_masters?
248
- errors << "Invalid node pools configuration"
249
- return
250
- end
251
- end
202
+ def validate_new_k3s_version
203
+ new_k3s_version = options[:new_k3s_version]
204
+ errors << 'The new k3s version is invalid' unless available_releases.include? new_k3s_version
205
+ end
252
206
 
253
- if worker_node_pools.nil? && schedule_workloads_on_masters?
254
- return
255
- end
207
+ def validate_masters
208
+ masters_pool = nil
256
209
 
257
- if !worker_node_pools.is_a? Array
258
- errors << "Invalid node pools configuration"
259
- elsif worker_node_pools.size == 0
260
- unless schedule_workloads_on_masters?
261
- errors << "At least one node pool is required in order to schedule workloads"
262
- end
263
- elsif worker_node_pools.map{ |worker_node_pool| worker_node_pool["name"]}.uniq.size != worker_node_pools.size
264
- errors << "Each node pool must have an unique name"
265
- elsif server_types
266
- worker_node_pools.each do |worker_node_pool|
267
- validate_instance_group worker_node_pool
268
- end
269
- end
210
+ begin
211
+ masters_pool = configuration['masters']
212
+ rescue StandardError
213
+ errors << 'Invalid masters configuration'
214
+ return
270
215
  end
271
216
 
272
- def schedule_workloads_on_masters?
273
- schedule_workloads_on_masters = configuration.dig("schedule_workloads_on_masters")
274
- schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
217
+ if masters_pool.nil?
218
+ errors << 'Invalid masters configuration'
219
+ return
275
220
  end
276
221
 
277
- def validate_instance_group(instance_group, workers: true)
278
- instance_group_errors = []
222
+ validate_instance_group masters_pool, workers: false
223
+ end
279
224
 
280
- instance_group_type = workers ? "Worker mode pool #{instance_group["name"]}" : "Masters pool"
225
+ def validate_worker_node_pools
226
+ worker_node_pools = configuration['worker_node_pools'] || []
281
227
 
282
- unless !workers || instance_group["name"] =~ /\A([A-Za-z0-9\-\_]+)\Z/
283
- instance_group_errors << "#{instance_group_type} has an invalid name"
284
- end
228
+ unless worker_node_pools.size.positive? || schedule_workloads_on_masters?
229
+ errors << 'Invalid node pools configuration'
230
+ return
231
+ end
285
232
 
286
- unless instance_group.is_a? Hash
287
- instance_group_errors << "#{instance_group_type} is in an invalid format"
233
+ return if worker_node_pools.size.zero? && schedule_workloads_on_masters?
234
+
235
+ if !worker_node_pools.is_a? Array
236
+ errors << 'Invalid node pools configuration'
237
+ elsif worker_node_pools.size.zero?
238
+ errors << 'At least one node pool is required in order to schedule workloads' unless schedule_workloads_on_masters?
239
+ elsif worker_node_pools.map { |worker_node_pool| worker_node_pool['name'] }.uniq.size != worker_node_pools.size
240
+ errors << 'Each node pool must have an unique name'
241
+ elsif server_types
242
+ worker_node_pools.each do |worker_node_pool|
243
+ validate_instance_group worker_node_pool
288
244
  end
245
+ end
246
+ end
289
247
 
290
- unless !valid_token? or server_types.include?(instance_group["instance_type"])
291
- instance_group_errors << "#{instance_group_type} has an invalid instance type"
292
- end
248
+ def schedule_workloads_on_masters?
249
+ schedule_workloads_on_masters = configuration['schedule_workloads_on_masters']
250
+ schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
251
+ end
293
252
 
294
- if instance_group["instance_count"].is_a? Integer
295
- if instance_group["instance_count"] < 1
296
- instance_group_errors << "#{instance_group_type} must have at least one node"
297
- elsif !workers
298
- instance_group_errors << "Masters count must equal to 1 for non-HA clusters or an odd number (recommended 3) for an HA cluster" unless instance_group["instance_count"].odd?
299
- end
300
- else
301
- instance_group_errors << "#{instance_group_type} has an invalid instance count"
253
+ def validate_instance_group(instance_group, workers: true)
254
+ instance_group_errors = []
255
+
256
+ instance_group_type = workers ? "Worker mode pool '#{instance_group['name']}'" : 'Masters pool'
257
+
258
+ instance_group_errors << "#{instance_group_type} has an invalid name" unless !workers || instance_group['name'] =~ /\A([A-Za-z0-9\-_]+)\Z/
259
+
260
+ instance_group_errors << "#{instance_group_type} is in an invalid format" unless instance_group.is_a? Hash
261
+
262
+ instance_group_errors << "#{instance_group_type} has an invalid instance type" unless !valid_token? || server_types.include?(instance_group['instance_type'])
263
+
264
+ if instance_group['instance_count'].is_a? Integer
265
+ if instance_group['instance_count'] < 1
266
+ instance_group_errors << "#{instance_group_type} must have at least one node"
267
+ elsif instance_group['instance_count'] > 10
268
+ instance_group_errors << "#{instance_group_type} cannot have more than 10 nodes due to a limitation with the Hetzner placement groups. You can add more node pools if you need more nodes."
269
+ elsif !workers
270
+ instance_group_errors << 'Masters count must equal to 1 for non-HA clusters or an odd number (recommended 3) for an HA cluster' unless instance_group['instance_count'].odd?
302
271
  end
272
+ else
273
+ instance_group_errors << "#{instance_group_type} has an invalid instance count"
274
+ end
303
275
 
304
- used_server_types << instance_group["instance_type"]
276
+ used_server_types << instance_group['instance_type']
305
277
 
306
- errors << instance_group_errors
278
+ errors << instance_group_errors
279
+ end
280
+
281
+ def validate_verify_host_key
282
+ return unless [true, false].include?(configuration.fetch('public_ssh_key_path', false))
283
+
284
+ errors << 'Please set the verify_host_key option to either true or false'
285
+ end
286
+
287
+ def hetzner_token
288
+ @token = ENV['HCLOUD_TOKEN']
289
+ return @token if @token
290
+
291
+ @token = configuration['hetzner_token']
292
+ end
293
+
294
+ def validate_ssh_allowed_networks
295
+ networks ||= configuration['ssh_allowed_networks']
296
+
297
+ if networks.nil? || networks.empty?
298
+ errors << 'At least one network/IP range must be specified for SSH access'
299
+ return
307
300
  end
308
301
 
309
- def validate_verify_host_key
310
- return unless [true, false].include?(configuration.fetch("public_ssh_key_path", false))
311
- errors << "Please set the verify_host_key option to either true or false"
302
+ invalid_networks = networks.reject do |network|
303
+ IPAddr.new(network)
304
+ rescue StandardError
305
+ false
312
306
  end
313
307
 
314
- def find_hetzner_token
315
- @token = ENV["HCLOUD_TOKEN"]
316
- return @token if @token
317
- @token = configuration.dig("hetzner_token")
308
+ unless invalid_networks.empty?
309
+ invalid_networks.each do |network|
310
+ errors << "The network #{network} is an invalid range"
311
+ end
318
312
  end
319
313
 
320
- def validate_ssh_allowed_networks
321
- networks ||= configuration.dig("ssh_allowed_networks")
314
+ invalid_ranges = networks.reject do |network|
315
+ network.include? '/'
316
+ end
322
317
 
323
- if networks.nil? or networks.empty?
324
- errors << "At least one network/IP range must be specified for SSH access"
325
- return
318
+ unless invalid_ranges.empty?
319
+ invalid_ranges.each do |_network|
320
+ errors << 'Please use the CIDR notation for the networks to avoid ambiguity'
326
321
  end
322
+ end
327
323
 
328
- invalid_networks = networks.reject do |network|
329
- IPAddr.new(network) rescue false
330
- end
324
+ return unless invalid_networks.empty?
331
325
 
332
- unless invalid_networks.empty?
333
- invalid_networks.each do |network|
334
- errors << "The network #{network} is an invalid range"
335
- end
336
- end
326
+ current_ip = URI.open('http://whatismyip.akamai.com').read
337
327
 
338
- invalid_ranges = networks.reject do |network|
339
- network.include? "/"
340
- end
328
+ current_ip_networks = networks.detect do |network|
329
+ IPAddr.new(network).include?(current_ip)
330
+ rescue StandardError
331
+ false
332
+ end
341
333
 
342
- unless invalid_ranges.empty?
343
- invalid_ranges.each do |network|
344
- errors << "Please use the CIDR notation for the networks to avoid ambiguity"
345
- end
346
- end
334
+ errors << "Your current IP #{current_ip} is not included into any of the networks you've specified, so we won't be able to SSH into the nodes" unless current_ip_networks
335
+ end
347
336
 
348
- return unless invalid_networks.empty?
337
+ def validate_additional_packages
338
+ additional_packages = configuration['additional_packages']
339
+ errors << 'Invalid additional packages configuration - it should be an array' if additional_packages && !additional_packages.is_a?(Array)
340
+ end
349
341
 
350
- current_ip = URI.open('http://whatismyip.akamai.com').read
342
+ def validate_create
343
+ validate_public_ssh_key
344
+ validate_private_ssh_key
345
+ validate_ssh_allowed_networks
346
+ validate_location
347
+ validate_k3s_version
348
+ validate_masters
349
+ validate_worker_node_pools
350
+ validate_verify_host_key
351
+ validate_additional_packages
352
+ end
351
353
 
352
- current_ip_networks = networks.detect do |network|
353
- IPAddr.new(network).include?(current_ip) rescue false
354
- end
354
+ def validate_upgrade
355
+ validate_kubeconfig_path_must_exist
356
+ validate_new_k3s_version
357
+ end
358
+
359
+ def validate_configuration_file
360
+ config_file_path = options[:config_file]
355
361
 
356
- unless current_ip_networks
357
- errors << "Your current IP #{current_ip} is not included into any of the networks you've specified, so we won't be able to SSH into the nodes"
362
+ if File.exist?(config_file_path)
363
+ begin
364
+ @configuration = YAML.load_file(options[:config_file])
365
+ unless configuration.is_a? Hash
366
+ puts 'Configuration is invalid'
367
+ exit 1
368
+ end
369
+ rescue StandardError
370
+ puts 'Please ensure that the config file is a correct YAML manifest.'
371
+ exit 1
358
372
  end
373
+ else
374
+ puts 'Please specify a correct path for the config file.'
375
+ exit 1
359
376
  end
360
-
377
+ end
361
378
  end
362
379
  end
363
380
  end