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,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