chef-licensing 0.4.44 → 1.0.0

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