chef-licensing 0.4.44 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/chef-licensing.gemspec +6 -3
  3. data/lib/chef-licensing/config.rb +9 -1
  4. data/lib/chef-licensing/context.rb +36 -1
  5. data/lib/chef-licensing/exceptions/invalid_file_format_version.rb +10 -0
  6. data/lib/chef-licensing/exceptions/license_file_corrupted.rb +9 -0
  7. data/lib/chef-licensing/exceptions/unsupported_content_type.rb +9 -0
  8. data/lib/chef-licensing/license.rb +5 -1
  9. data/lib/chef-licensing/license_key_fetcher/chef_licensing_interactions.yaml +85 -234
  10. data/lib/chef-licensing/license_key_fetcher/file.rb +71 -14
  11. data/lib/chef-licensing/license_key_fetcher/license_file/base.rb +63 -0
  12. data/lib/chef-licensing/license_key_fetcher/license_file/v3.rb +12 -0
  13. data/lib/chef-licensing/license_key_fetcher/license_file/v4.rb +27 -0
  14. data/lib/chef-licensing/license_key_fetcher.rb +45 -20
  15. data/lib/chef-licensing/list_license_keys.rb +9 -3
  16. data/lib/chef-licensing/restful_client/base.rb +62 -26
  17. data/lib/chef-licensing/restful_client/middleware/content_type_validator.rb +24 -0
  18. data/lib/chef-licensing/restful_client/middleware/exceptions_handler.rb +1 -3
  19. data/lib/chef-licensing/restful_client/v1.rb +0 -2
  20. data/lib/chef-licensing/tui_engine/tui_actions.rb +11 -88
  21. data/lib/chef-licensing/version.rb +1 -1
  22. data/lib/chef-licensing.rb +5 -0
  23. metadata +16 -13
  24. data/LICENSE +0 -1
  25. data/lib/chef-licensing/exceptions/license_generation_failed.rb +0 -9
  26. data/lib/chef-licensing/exceptions/license_generation_rejected.rb +0 -7
  27. data/lib/chef-licensing/license_key_generator.rb +0 -47
@@ -5,6 +5,10 @@ require "date"
5
5
  require "fileutils" unless defined?(FileUtils)
6
6
  require_relative "../license_key_fetcher"
7
7
  require_relative "../config"
8
+ require_relative "../exceptions/license_file_corrupted"
9
+ require_relative "license_file/v4"
10
+ require_relative "license_file/v3"
11
+ require_relative "../exceptions/invalid_file_format_version"
8
12
 
9
13
  module ChefLicensing
10
14
  class LicenseKeyFetcher
@@ -63,6 +67,15 @@ module ChefLicensing
63
67
  @active_trial_status
64
68
  end
65
69
 
70
+ def user_has_active_trial_or_free_license?
71
+ read_license_key_file
72
+ return false unless contents&.key?(:licenses)
73
+
74
+ all_license_keys = contents[:licenses].collect { |license| license[:license_key] }
75
+ license_obj = ChefLicensing.client(license_keys: all_license_keys)
76
+ (%w{trial free}.include? license_obj.license_type&.downcase) && license_obj.active?
77
+ end
78
+
66
79
  def fetch_allowed_license_types_for_addition
67
80
  license_types = %i{free trial commercial}
68
81
  existing_license_types = fetch_license_types
@@ -118,15 +131,15 @@ module ChefLicensing
118
131
 
119
132
  @contents = load_license_file(license_key_file_path)
120
133
 
134
+ # Two possible cases:
135
+ # 1. If contents is nil, load basic license data with the latest structure.
136
+ # 2. If contents is not nil, but the license server URL in contents is different from the system's,
137
+ # update the license server URL in contents and licenses.yaml file.
121
138
  if @contents.nil?
122
- @contents = {
123
- file_format_version: LICENSE_FILE_FORMAT_VERSION,
124
- license_server_url: license_server_url_from_system || license_server_url_from_config,
125
- }
126
- else
127
- if license_server_url_from_system && license_server_url_from_system != @contents[:license_server_url]
128
- @contents[:license_server_url] = license_server_url_from_system
129
- end
139
+ url = license_server_url_from_system || license_server_url_from_config
140
+ load_basic_license_data_to_contents(url, [])
141
+ elsif @contents && license_server_url_from_system && license_server_url_from_system != @contents[:license_server_url]
142
+ @contents[:license_server_url] = license_server_url_from_system
130
143
  end
131
144
 
132
145
  # Ensure the license server URL is returned to the caller in all cases
@@ -207,11 +220,27 @@ module ChefLicensing
207
220
 
208
221
  # only checking for major version for file format for breaking changes
209
222
  @contents ||= YAML.load(::File.read(path))
223
+
224
+ # raise error if the file_format_version key is missing
225
+ raise LicenseFileCorrupted.new("Unrecognized license file; :file_format_version missing.") unless @contents.key?(:file_format_version)
226
+
227
+ # Three possible cases after loading the license file contents:
228
+ # 1. If the file format version is the same as the current version (latest), verify the structure and return the contents.
229
+ # 2. If the file format version is different but supported, migrate the contents to the current version and return them.
230
+ # 3. If the file format version is different and not supported, raise an error.
210
231
  if major_version(@contents[:file_format_version]) == major_version(LICENSE_FILE_FORMAT_VERSION)
232
+ current_version_class_name = get_license_file_class(LICENSE_FILE_FORMAT_VERSION)
233
+ # we ignore any additional keys in the license file during verification
234
+ raise LicenseFileCorrupted.new("Invalid data found in the license file.") unless current_version_class_name.send(:verify_structure, @contents)
235
+
236
+ @contents
237
+ elsif license_file_class_exists?(@contents[:file_format_version])
238
+ @contents = migrate_license_file_content_to_current_version(@contents)
239
+ write_license_file(path) # update the license file contents to the latest version
211
240
  @contents
212
241
  else
213
242
  logger.debug "License File version #{@contents[:file_format_version]} not supported."
214
- raise LicenseKeyNotFetchedError.new("License File version #{@contents[:file_format_version]} not supported.")
243
+ raise ChefLicensing::InvalidFileFormatVersion.new("Unable to read licenses. License File version #{@contents[:file_format_version]} not supported.")
215
244
  end
216
245
  end
217
246
 
@@ -244,11 +273,7 @@ module ChefLicensing
244
273
 
245
274
  logger.debug "Loading license data to contents"
246
275
  if @contents.nil? || @contents.empty? # this case is likely to happen only during testing
247
- @contents = {
248
- file_format_version: LICENSE_FILE_FORMAT_VERSION,
249
- license_server_url: @license_server_url,
250
- licenses: [license_data],
251
- }
276
+ load_basic_license_data_to_contents(@license_server_url, [license_data])
252
277
  elsif @contents[:licenses].nil?
253
278
  @contents[:licenses] = [license_data]
254
279
  elsif fetch_license_keys(@contents[:licenses])&.include?(license_data[:license_key])
@@ -270,6 +295,38 @@ module ChefLicensing
270
295
  logger.debug "#{e.backtrace.join("\n\t")}"
271
296
  e
272
297
  end
298
+
299
+ # Returns the license file class for the given version.
300
+ def get_license_file_class(version)
301
+ Object.const_get("ChefLicensing::LicenseFile::V#{major_version(version)}")
302
+ end
303
+
304
+ # Returns true if the license file class for the given version exists.
305
+ def license_file_class_exists?(version)
306
+ Object.const_defined?("ChefLicensing::LicenseFile::V#{major_version(version)}")
307
+ end
308
+
309
+ # Loads the basic license data to contents in the current version's structure.
310
+ def load_basic_license_data_to_contents(url, license_data = [])
311
+ current_version_class_name = get_license_file_class(LICENSE_FILE_FORMAT_VERSION)
312
+ @contents = current_version_class_name.send(:load_primary_structure)
313
+ @contents[:file_format_version] = LICENSE_FILE_FORMAT_VERSION
314
+ @contents[:license_server_url] = url || ""
315
+ @contents[:licenses] = license_data
316
+ end
317
+
318
+ # Migrates the license file content to the current version and returns the migrated contents.
319
+ def migrate_license_file_content_to_current_version(contents)
320
+ logger.warn "License File version #{contents[:file_format_version]} is deprecated."
321
+ logger.warn "Automatically migrating license file to version #{LICENSE_FILE_FORMAT_VERSION}."
322
+ given_version_class_name = get_license_file_class(contents[:file_format_version])
323
+ # we ignore any additional keys in the license file during verification
324
+ raise LicenseFileCorrupted.new("Invalid data found in the license file.") unless given_version_class_name.send(:verify_structure, contents)
325
+
326
+ current_version_class_name = get_license_file_class(LICENSE_FILE_FORMAT_VERSION)
327
+ contents = current_version_class_name.send(:migrate_structure, contents, major_version(contents[:file_format_version]))
328
+ contents
329
+ end
273
330
  end
274
331
  end
275
332
  end
@@ -0,0 +1,63 @@
1
+ module ChefLicensing
2
+ module LicenseFile
3
+ class Base
4
+ EXPECTED_STRUCTURE = {
5
+ file_format_version: "0.0.0",
6
+ licenses: [
7
+ {
8
+ license_key: String,
9
+ license_type: Symbol,
10
+ update_time: String,
11
+ },
12
+ ],
13
+ }.freeze
14
+
15
+ # @param [Hash] data: The data to verify
16
+ # @param [Hash] expected_structure: The structure to verify against
17
+ # @return [Boolean] true if the data matches the expected structure, false otherwise
18
+ # @note This method ignores extra keys in the data that are not in the expected structure
19
+ def self.verify_structure(data, expected_structure = self::EXPECTED_STRUCTURE)
20
+ return false unless data.is_a?(Hash)
21
+
22
+ expected_structure.each do |key, value|
23
+ return false unless data.key?(key)
24
+
25
+ if value.is_a?(Hash)
26
+ return false unless verify_structure(data[key], value)
27
+ elsif value.is_a?(Array)
28
+ return false unless data[key].is_a?(Array)
29
+
30
+ data[key].each do |item|
31
+ return false unless verify_structure(item, value[0])
32
+ end
33
+ elsif value.is_a?(Class)
34
+ return false unless data[key].is_a?(value)
35
+ else
36
+ return false unless data[key] == value
37
+ end
38
+ end
39
+
40
+ true
41
+ end
42
+
43
+ # @return [Hash] The primary structure of the license file, without nested structures
44
+ def self.load_primary_structure
45
+ expected_structure_dup = self::EXPECTED_STRUCTURE.dup
46
+ expected_structure_dup[:licenses] = []
47
+ expected_structure_dup
48
+ end
49
+
50
+ # @return [Hash] The complete structure of the license file, including nested structures
51
+ def self.load_structure
52
+ self::EXPECTED_STRUCTURE
53
+ end
54
+
55
+ # @param [Hash] contents: The contents of the license file
56
+ # @param [Integer] version: The version of the license file
57
+ # @return [Hash] The contents of the license file after migration
58
+ def self.migrate_structure(contents, version)
59
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "base"
2
+
3
+ module ChefLicensing
4
+ module LicenseFile
5
+ class V3 < Base
6
+ LICENSE_FILE_FORMAT_VERSION = "3.0.0".freeze
7
+ EXPECTED_STRUCTURE = EXPECTED_STRUCTURE.merge({
8
+ file_format_version: V3::LICENSE_FILE_FORMAT_VERSION,
9
+ }).freeze
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "base"
2
+ require_relative "../../config"
3
+
4
+ module ChefLicensing
5
+ module LicenseFile
6
+ class V4 < Base
7
+ LICENSE_FILE_FORMAT_VERSION = "4.0.0".freeze
8
+
9
+ EXPECTED_STRUCTURE = EXPECTED_STRUCTURE.merge({
10
+ file_format_version: V4::LICENSE_FILE_FORMAT_VERSION,
11
+ license_server_url: String,
12
+ }).freeze
13
+
14
+ # @param [Hash] contents: The contents of the license file
15
+ # @param [Integer] version: The version of the license file
16
+ # @return [Hash] The contents of the license file after migration
17
+ def self.migrate_structure(contents, version)
18
+ # Backwards compatibility for version 3 license files
19
+ if version == 3
20
+ contents[:license_server_url] = ChefLicensing::Config.license_server_url || ""
21
+ contents[:file_format_version] = V4::LICENSE_FILE_FORMAT_VERSION
22
+ end
23
+ contents
24
+ end
25
+ end
26
+ end
27
+ end
@@ -71,7 +71,9 @@ module ChefLicensing
71
71
 
72
72
  unless @license_keys.empty?
73
73
  # Licenses expiration check
74
- if licenses_active?
74
+ # Client API possible errors will be handled in software entitlement check call (made after this)
75
+ # client_api_call_error is set to true when there is an error in licenses_active? call
76
+ if licenses_active? || client_api_call_error
75
77
  return @license_keys
76
78
  else
77
79
  # Prompts if the keys are expired or expiring
@@ -83,9 +85,9 @@ module ChefLicensing
83
85
  end
84
86
  end
85
87
 
86
- # Scenario: When a user is prompted for license expiry and license is not yet renewed
87
- if %i{prompt_license_about_to_expire prompt_license_expired_local_mode}.include?(config[:start_interaction])
88
- # Not blocking any license type in case of expiry
88
+ # Expired trial licenses and exhausted free licenses will be blocked
89
+ # Not blocking commercial licenses
90
+ if license && ((!license.expired? && !license.exhausted?) || (license.license_type.downcase == "commercial"))
89
91
  return @license_keys
90
92
  end
91
93
 
@@ -116,7 +118,8 @@ module ChefLicensing
116
118
  # Return keys if license keys are active and not expired or expiring
117
119
  # Return keys if there is any error in /client API call, and do not block the flow.
118
120
  # Client API possible errors will be handled in software entitlement check call (made after this)
119
- return @license_keys if (!@license_keys.empty? && licenses_active?) || client_api_call_error
121
+ # client_api_call_error is set to true when there is an error in licenses_active? call
122
+ return @license_keys if (!@license_keys.empty? && licenses_active? && ChefLicensing::Context.license.license_type.downcase == "commercial") || client_api_call_error
120
123
 
121
124
  # Lowest priority is to interactively prompt if we have a TTY
122
125
  if config[:output].isatty
@@ -128,13 +131,19 @@ module ChefLicensing
128
131
  # If license type is not selected using TUI, assign it using API call to fetch type.
129
132
  prompt_fetcher.license_type ||= get_license_type(new_keys.first)
130
133
  persist_and_concat(new_keys, prompt_fetcher.license_type)
131
- return license_keys
134
+ license ||= ChefLicensing::Context.license
135
+ # Expired trial licenses and exhausted free licenses will be blocked
136
+ # Not blocking commercial licenses
137
+ if (!license&.expired? && !license&.exhausted?) || (license&.license_type&.downcase == "commercial")
138
+ return license_keys
139
+ end
132
140
  end
133
141
  end
134
142
 
135
- # Scenario: When a user is prompted for license expiry and license is not yet renewed
136
- if new_keys.empty? && %i{prompt_license_about_to_expire prompt_license_expired}.include?(config[:start_interaction])
137
- # Not blocking any license type in case of expiry
143
+ # Expired trial licenses and exhausted free licenses will be blocked
144
+ # Not blocking commercial licenses
145
+ license ||= ChefLicensing::Context.license
146
+ if new_keys.empty? && license && ((!license.expired? && !license.exhausted?) || (license.license_type.downcase == "commercial"))
138
147
  return @license_keys
139
148
  end
140
149
 
@@ -195,6 +204,7 @@ module ChefLicensing
195
204
  extra_info[:license_type] = license.license_type.capitalize
196
205
  extra_info[:number_of_days_in_expiration] = license.number_of_days_in_expiration
197
206
  extra_info[:license_expiration_date] = Date.parse(license.expiration_date).strftime("%a, %d %b %Y")
207
+ extra_info[:is_commercial] = license.license_type.downcase == "commercial"
198
208
  end
199
209
 
200
210
  unless info.empty? # ability to add info hash through arguments
@@ -212,6 +222,9 @@ module ChefLicensing
212
222
  # This call returns a license based on client logic
213
223
  # This API call is only made when multiple license keys are present or if client call was never done
214
224
  self.license = ChefLicensing.client(license_keys: @license_keys) if !license || @license_keys.count > 1
225
+
226
+ # Cache license context
227
+ ChefLicensing::Context.license = license
215
228
  # Intentional lag of 2 seconds when license is expiring or expired
216
229
  sleep 2 if license.expiring_or_expired?
217
230
  spinner.success # Stop the spinner
@@ -227,7 +240,13 @@ module ChefLicensing
227
240
  config[:start_interaction] = :prompt_license_about_to_expire
228
241
  prompt_fetcher.config = config
229
242
  false
243
+ elsif license.exhausted? && (license.license_type.downcase == "commercial" || license.license_type.downcase == "free")
244
+ config[:start_interaction] = :prompt_license_exhausted
245
+ prompt_fetcher.config = config
246
+ false
230
247
  else
248
+ # If license is not expired or expiring, return true. But if the license is not commercial, warn the user.
249
+ config[:start_interaction] = :warn_non_commercial_license unless license.license_type.downcase == "commercial"
231
250
  true
232
251
  end
233
252
  rescue ChefLicensing::ClientError => e
@@ -271,8 +290,8 @@ module ChefLicensing
271
290
  end
272
291
 
273
292
  def get_license_type(license_key)
274
- self.license = ChefLicensing.client(license_keys: [license_key])
275
- license.license_type.downcase.to_sym
293
+ license_obj = ChefLicensing.client(license_keys: [license_key])
294
+ license_obj.license_type.downcase.to_sym
276
295
  end
277
296
 
278
297
  def license_restricted?(license_type)
@@ -283,7 +302,7 @@ module ChefLicensing
283
302
  def prompt_license_addition_restricted(license_type, existing_license_keys_in_file)
284
303
  logger.debug "License Key fetcher - prompting license addition restriction"
285
304
  # For trial license
286
- # TODO for free license
305
+ # TODO for Free Tier License
287
306
  config[:start_interaction] = :prompt_license_addition_restriction
288
307
  prompt_fetcher.config = config
289
308
  # Existing license keys are needed to show details of existing license of license type which is restricted.
@@ -294,23 +313,29 @@ module ChefLicensing
294
313
  def unrestricted_license_added?(new_keys, license_type)
295
314
  if license_restricted?(license_type)
296
315
  # Existing license keys of same license type are fetched to compare if old license key or a new one is added.
297
- # However, if user is trying to add free license, and user has active trial license, we fetch the trial license key
316
+ # However, if user is trying to add Free Tier License, and user has active trial license, we fetch the trial license key
298
317
  if license_type == :free && file_fetcher.user_has_active_trial_license?
299
318
  existing_license_keys_in_file = file_fetcher.fetch_license_keys_based_on_type(:trial)
300
- else
319
+ elsif file_fetcher.user_has_active_trial_or_free_license?
320
+ # Handling license addition restriction scenarios only if the current license is an active license
301
321
  existing_license_keys_in_file = file_fetcher.fetch_license_keys_based_on_type(license_type)
302
322
  end
323
+
303
324
  # Only prompt when a new trial license is added
304
- unless existing_license_keys_in_file.last == new_keys.first
305
- # prompt the message that this addition of license is restricted.
306
- prompt_license_addition_restricted(license_type, existing_license_keys_in_file)
307
- return false
325
+ if existing_license_keys_in_file
326
+ unless existing_license_keys_in_file.last == new_keys.first
327
+ # prompt the message that this addition of license is restricted.
328
+ prompt_license_addition_restricted(license_type, existing_license_keys_in_file)
329
+ return false
330
+ end
308
331
  end
309
- true # license type is restricted but not the key since it is the same key hence returning true
332
+ # license addition should be restricted but it is not because the key is same as that of one user is trying to add
333
+ # license addition should be restricted but it is not because the license is expired and warning wont be handled by this restriction
334
+ true
310
335
  else
311
336
  persist_and_concat(new_keys, license_type)
312
337
  true
313
338
  end
314
339
  end
315
340
  end
316
- end
341
+ end
@@ -28,8 +28,11 @@ module ChefLicensing
28
28
 
29
29
  licenses_metadata.each do |license|
30
30
  puts_bold "License Key : #{license.id}"
31
+ # Note: The license type is returned as "free" for Free Tier Licenses from the server.
32
+ # This is capitalized to "Free Tier" for display purposes as recommended by the product team.
33
+ license_type = license.license_type == "free" ? "Free Tier" : license.license_type.capitalize
31
34
  output.puts <<~LICENSE
32
- Type : #{license.license_type}
35
+ Type : #{license_type}
33
36
  Status : #{license.status}
34
37
  Expiration Date : #{license.expiration_date}
35
38
 
@@ -57,7 +60,10 @@ module ChefLicensing
57
60
  def display_overview
58
61
  output.puts "------------------------------------------------------------"
59
62
  licenses_metadata.each do |license|
60
- # Sets the validity text for a free license as "Unlimited" and displays the number of days for others.
63
+ # Note: The license type is returned as "free" for Free Tier Licenses from the server.
64
+ # This is capitalized to "Free Tier" for display purposes as recommended by the product team.
65
+ license_type = license.license_type == "free" ? "Free Tier" : license.license_type.capitalize
66
+ # Sets the validity text for a Free Tier License as "Unlimited" and displays the number of days for others.
61
67
  validity = if license.license_type == "free"
62
68
  "Unlimited"
63
69
  else
@@ -72,7 +78,7 @@ module ChefLicensing
72
78
  #{pastel.bold("License Details")}
73
79
  Asset Name : #{license.limits.first.software}
74
80
  License ID : #{license.id}
75
- Type : #{license.license_type.capitalize}
81
+ Type : #{license_type}
76
82
  Status : #{license.status.capitalize}
77
83
  Validity : #{validity}
78
84
  No. Of Units : #{num_of_units} #{unit_measure.capitalize.pluralize(num_of_units)}
@@ -7,6 +7,7 @@ require_relative "../exceptions/restful_client_connection_error"
7
7
  require_relative "../exceptions/missing_api_credentials_error"
8
8
  require_relative "../config"
9
9
  require_relative "middleware/exceptions_handler"
10
+ require_relative "middleware/content_type_validator"
10
11
 
11
12
  module ChefLicensing
12
13
  module RestfulClient
@@ -22,6 +23,7 @@ module ChefLicensing
22
23
  }.freeze
23
24
 
24
25
  CURRENT_ENDPOINT_VERSION = 2
26
+ REQUEST_LIMIT = 5
25
27
 
26
28
  def initialize
27
29
  raise MissingAPICredentialsError, "Missing credential in config: Set in block chef_license_server or use environment variable CHEF_LICENSE_SERVER or pass through argument --chef-license-server" if ChefLicensing::Config.license_server_url.nil?
@@ -33,14 +35,6 @@ module ChefLicensing
33
35
  invoke_get_api(self.class::END_POINTS[:VALIDATE], { licenseId: license, version: CURRENT_ENDPOINT_VERSION })
34
36
  end
35
37
 
36
- def generate_trial_license(payload)
37
- invoke_post_api(self.class::END_POINTS[:GENERATE_TRIAL_LICENSE], payload)
38
- end
39
-
40
- def generate_free_license(payload)
41
- invoke_post_api(self.class::END_POINTS[:GENERATE_FREE_LICENSE], payload)
42
- end
43
-
44
38
  def feature_by_name(payload)
45
39
  invoke_post_api(self.class::END_POINTS[:FEATURE_BY_NAME], payload)
46
40
  end
@@ -75,53 +69,82 @@ module ChefLicensing
75
69
 
76
70
  # a common method to handle the get API calls
77
71
  def invoke_get_api(endpoint, params = {})
78
- handle_get_connection do |connection|
79
- connection.get(endpoint, params).body
80
- end
72
+ response = invoke_api(ChefLicensing::Config.license_server_url.split(","), endpoint, :get, nil, params)
73
+ response.body
81
74
  end
82
75
 
83
76
  # a common method to handle the post API calls
84
77
  def invoke_post_api(endpoint, payload, headers = {})
85
- handle_post_connection do |connection|
86
- response = connection.post(endpoint) do |request|
87
- request.body = payload.to_json
88
- request.headers = headers
78
+ response = invoke_api(ChefLicensing::Config.license_server_url.split(","), endpoint, :post, payload, nil, headers)
79
+ raise RestfulClientError, format_error_from(response) unless response.success?
80
+
81
+ response.body
82
+ end
83
+
84
+ def invoke_api(urls, endpoint, http_method, payload = nil, params = {}, headers = {})
85
+ handle_connection = http_method == :get ? method(:handle_get_connection) : method(:handle_post_connection)
86
+ response = nil
87
+ attempted_urls = []
88
+
89
+ logger.warn "Only the first #{REQUEST_LIMIT} urls will be tried." if urls.size > REQUEST_LIMIT
90
+ urls.each_with_index do |url, i|
91
+ url = url.strip
92
+ attempted_urls << url
93
+ break if i == REQUEST_LIMIT - 1
94
+
95
+ logger.debug "Trying to connect to #{url}"
96
+ handle_connection.call(url) do |connection|
97
+ response = connection.send(http_method, endpoint) do |request|
98
+ request.body = payload.to_json if payload
99
+ request.params = params if params
100
+ request.headers = headers if headers
101
+ end
89
102
  end
90
- raise RestfulClientError, format_error_from(response) unless response.success?
91
-
92
- response.body
103
+ # At this point, we have a successful connection
104
+ # Update the value of license server url in config
105
+ ChefLicensing::Config.license_server_url = url
106
+ logger.debug "Connection succeeded to #{url}"
107
+ break response
108
+ rescue RestfulClientConnectionError
109
+ logger.warn "Connection failed to #{url}"
110
+ rescue URI::InvalidURIError
111
+ logger.warn "Invalid URI #{url}"
93
112
  end
113
+
114
+ raise_restful_client_conn_error(attempted_urls) if response.nil?
115
+ response
94
116
  end
95
117
 
96
- def handle_get_connection
118
+ def handle_get_connection(url = nil)
97
119
  # handle faraday errors
98
- yield get_connection
120
+ yield get_connection(url)
99
121
  rescue Faraday::ClientError => e
100
122
  logger.debug "Restful Client Error #{e.message}"
101
123
  raise RestfulClientError, e.message
102
124
  end
103
125
 
104
- def handle_post_connection
126
+ def handle_post_connection(url = nil)
105
127
  # handle faraday errors
106
- yield post_connection
128
+ yield post_connection(url)
107
129
  rescue Faraday::ClientError => e
108
130
  logger.debug "Restful Client Error #{e.message}"
109
131
  raise RestfulClientError, e.message
110
132
  end
111
133
 
112
- def get_connection
134
+ def get_connection(url = nil)
113
135
  store = ::ActiveSupport::Cache.lookup_store(:file_store, Dir.tmpdir)
114
- Faraday.new(url: ChefLicensing::Config.license_server_url) do |config|
136
+ Faraday.new(url: url) do |config|
115
137
  config.request :json
116
138
  config.response :json, parser_options: { object_class: OpenStruct }
117
139
  config.use Faraday::HttpCache, shared_cache: false, logger: logger, store: store
118
140
  config.use Middleware::ExceptionsHandler
141
+ config.use Middleware::ContentTypeValidator
119
142
  config.adapter Faraday.default_adapter
120
143
  end
121
144
  end
122
145
 
123
- def post_connection
124
- Faraday.new(url: ChefLicensing::Config.license_server_url) do |config|
146
+ def post_connection(url = nil)
147
+ Faraday.new(url: url) do |config|
125
148
  config.request :json
126
149
  config.response :json, parser_options: { object_class: OpenStruct }
127
150
  config.use Middleware::ExceptionsHandler
@@ -134,6 +157,19 @@ module ChefLicensing
134
157
 
135
158
  error_details
136
159
  end
160
+
161
+ def raise_restful_client_conn_error(urls)
162
+ error_message = <<~EOM
163
+ Unable to connect to the licensing server. #{ChefLicensing::Config.chef_product_name} requires server communication to operate.
164
+ The following URL(s) were tried:\n#{
165
+ urls.each_with_index.map do |url, index|
166
+ "#{index + 1}. #{url}"
167
+ end.join("\n")
168
+ }
169
+ EOM
170
+
171
+ raise RestfulClientConnectionError, error_message
172
+ end
137
173
  end
138
174
  end
139
175
  end
@@ -0,0 +1,24 @@
1
+ require "faraday/middleware"
2
+ require_relative "../../../chef-licensing/exceptions/unsupported_content_type"
3
+
4
+ module Middleware
5
+ class ContentTypeValidator < Faraday::Middleware
6
+ def call(env)
7
+ @app.call(env).on_complete do |response_env|
8
+ content_type = response_env[:response_headers]["content-type"]
9
+ body = response_env[:body]
10
+ # trim the body to 1000 characters to avoid printing a huge string in the error message
11
+ body = body[0..1000] if body.is_a?(String)
12
+ raise ChefLicensing::UnsupportedContentType, error_message(content_type, body) unless content_type == "application/json"
13
+ end
14
+ end
15
+
16
+ def error_message(content_type, body = nil)
17
+ <<~EOM
18
+ Expected 'application/json' content-type, but received '#{content_type}' from the licensing server.
19
+ Snippet of body: `#{body}`
20
+ Possible causes: Check for firewall restrictions, ensure proper server response, or seek support assistance.
21
+ EOM
22
+ end
23
+ end
24
+ end
@@ -8,9 +8,7 @@ module Middleware
8
8
  def call(env)
9
9
  @app.call(env)
10
10
  rescue Faraday::ConnectionFailed => e
11
- ChefLicensing::Config.logger.debug("Connection failed to #{ChefLicensing::Config.license_server_url} with error: #{e.message}")
12
- error_message = "Unable to connect to the licensing server at #{ChefLicensing::Config.license_server_url}.\nPlease check if the server is reachable and try again. #{ChefLicensing::Config.chef_product_name} requires server communication to operate."
13
- raise ChefLicensing::RestfulClientConnectionError, error_message
11
+ raise ChefLicensing::RestfulClientConnectionError, e.message
14
12
  end
15
13
  end
16
14
  end
@@ -6,8 +6,6 @@ module ChefLicensing
6
6
  class V1 < Base
7
7
  END_POINTS = END_POINTS.merge({
8
8
  VALIDATE: "v1/validate",
9
- GENERATE_TRIAL_LICENSE: "v1/trial",
10
- GENERATE_FREE_LICENSE: "v1/free",
11
9
  CLIENT: "v1/client",
12
10
  DESCRIBE: "v1/desc",
13
11
  LIST_LICENSES: "v1/listLicenses",