nucleus 0.2.0 → 0.3.1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +9 -0
  4. data/README.md +43 -72
  5. data/lib/nucleus/adapter_resolver.rb +3 -3
  6. data/lib/nucleus/adapters/base_adapter.rb +109 -109
  7. data/lib/nucleus/adapters/v1/cloud_foundry_v2/application.rb +111 -111
  8. data/lib/nucleus/adapters/v1/cloud_foundry_v2/cloud_foundry_v2.rb +141 -141
  9. data/lib/nucleus/adapters/v1/cloud_foundry_v2/data.rb +97 -97
  10. data/lib/nucleus/adapters/v1/cloud_foundry_v2/domains.rb +5 -5
  11. data/lib/nucleus/adapters/v1/cloud_foundry_v2/lifecycle.rb +41 -41
  12. data/lib/nucleus/adapters/v1/cloud_foundry_v2/logs.rb +6 -6
  13. data/lib/nucleus/adapters/v1/cloud_foundry_v2/regions.rb +33 -33
  14. data/lib/nucleus/adapters/v1/cloud_foundry_v2/services.rb +6 -6
  15. data/lib/nucleus/adapters/v1/cloud_foundry_v2/vars.rb +80 -80
  16. data/lib/nucleus/adapters/v1/heroku/app_states.rb +57 -57
  17. data/lib/nucleus/adapters/v1/heroku/data.rb +78 -78
  18. data/lib/nucleus/adapters/v1/heroku/heroku.rb +146 -146
  19. data/lib/nucleus/adapters/v1/heroku/lifecycle.rb +51 -51
  20. data/lib/nucleus/adapters/v1/heroku/logs.rb +2 -2
  21. data/lib/nucleus/adapters/v1/heroku/regions.rb +42 -42
  22. data/lib/nucleus/adapters/v1/heroku/services.rb +168 -168
  23. data/lib/nucleus/adapters/v1/heroku/vars.rb +65 -65
  24. data/lib/nucleus/adapters/v1/openshift_v2/app_states.rb +68 -68
  25. data/lib/nucleus/adapters/v1/openshift_v2/application.rb +1 -1
  26. data/lib/nucleus/adapters/v1/openshift_v2/data.rb +96 -96
  27. data/lib/nucleus/adapters/v1/openshift_v2/lifecycle.rb +60 -60
  28. data/lib/nucleus/adapters/v1/openshift_v2/logs.rb +106 -106
  29. data/lib/nucleus/adapters/v1/openshift_v2/openshift_v2.rb +125 -125
  30. data/lib/nucleus/adapters/v1/openshift_v2/regions.rb +58 -58
  31. data/lib/nucleus/adapters/v1/openshift_v2/services.rb +173 -173
  32. data/lib/nucleus/adapters/v1/openshift_v2/vars.rb +49 -49
  33. data/lib/nucleus/adapters/v1/stub_adapter.rb +464 -464
  34. data/lib/nucleus/core/adapter_extensions/auth/auth_client.rb +44 -44
  35. data/lib/nucleus/core/adapter_extensions/auth/expiring_token_auth_client.rb +53 -53
  36. data/lib/nucleus/core/adapter_extensions/auth/http_basic_auth_client.rb +3 -3
  37. data/lib/nucleus/core/adapter_extensions/auth/o_auth2_auth_client.rb +95 -95
  38. data/lib/nucleus/core/adapter_extensions/auth/token_auth_client.rb +36 -36
  39. data/lib/nucleus/core/adapter_extensions/http_client.rb +5 -5
  40. data/lib/nucleus/core/common/files/archive_extractor.rb +1 -1
  41. data/lib/nucleus/core/common/files/archiver.rb +2 -2
  42. data/lib/nucleus/core/file_handling/file_manager.rb +64 -64
  43. data/lib/nucleus/core/file_handling/git_deployer.rb +133 -133
  44. data/lib/nucleus/core/import/adapter_configuration.rb +53 -53
  45. data/lib/nucleus/scripts/initialize_config_defaults.rb +26 -26
  46. data/lib/nucleus/version.rb +1 -1
  47. data/nucleus.gemspec +2 -2
  48. data/spec/integration/api/auth_spec.rb +3 -3
  49. data/spec/spec_helper.rb +98 -98
  50. data/spec/test_suites.rake +1 -1
  51. data/spec/unit/adapters/git_deployer_spec.rb +262 -262
  52. data/spec/unit/common/helpers/auth_helper_spec.rb +1 -1
  53. data/tasks/evaluation.rake +1 -1
  54. data/wiki/adapter_tests.md +0 -7
  55. data/wiki/implement_new_adapter.md +1 -1
  56. metadata +4 -20
  57. data/config/adapters/cloud_control.yml +0 -32
  58. data/lib/nucleus/adapters/v1/cloud_control/application.rb +0 -108
  59. data/lib/nucleus/adapters/v1/cloud_control/authentication.rb +0 -27
  60. data/lib/nucleus/adapters/v1/cloud_control/buildpacks.rb +0 -23
  61. data/lib/nucleus/adapters/v1/cloud_control/cloud_control.rb +0 -153
  62. data/lib/nucleus/adapters/v1/cloud_control/data.rb +0 -76
  63. data/lib/nucleus/adapters/v1/cloud_control/domains.rb +0 -68
  64. data/lib/nucleus/adapters/v1/cloud_control/lifecycle.rb +0 -27
  65. data/lib/nucleus/adapters/v1/cloud_control/log_poller.rb +0 -71
  66. data/lib/nucleus/adapters/v1/cloud_control/logs.rb +0 -103
  67. data/lib/nucleus/adapters/v1/cloud_control/regions.rb +0 -32
  68. data/lib/nucleus/adapters/v1/cloud_control/scaling.rb +0 -17
  69. data/lib/nucleus/adapters/v1/cloud_control/semantic_errors.rb +0 -31
  70. data/lib/nucleus/adapters/v1/cloud_control/services.rb +0 -162
  71. data/lib/nucleus/adapters/v1/cloud_control/token.rb +0 -17
  72. data/lib/nucleus/adapters/v1/cloud_control/vars.rb +0 -88
@@ -1,51 +1,51 @@
1
- module Nucleus
2
- module Adapters
3
- module V1
4
- class Heroku < Stub
5
- module Lifecycle
6
- # Lifecycle:
7
- # A) via maintenance - workers are still active
8
- # B) via formation - loose scaling information
9
-
10
- # @see Stub#start
11
- def start(application_id)
12
- log.debug "Start @ #{@endpoint_url}"
13
-
14
- app = application(application_id)
15
- if app[:state] == Enums::ApplicationStates::DEPLOYED
16
- # add web dyno if there currently are no dynos (state == deployed)
17
- scale_web(application_id, 1)
18
- elsif app[:state] == Enums::ApplicationStates::CREATED
19
- # fail if there is no deployment
20
- fail Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be started'
21
- end
22
-
23
- scale_worker(application_id, 1)
24
- update_application(application_id, maintenance: false)
25
- end
26
-
27
- # @see Stub#stop
28
- def stop(application_id)
29
- log.debug "Stop @ #{@endpoint_url}"
30
-
31
- # fail if there is no deployment
32
- app = application(application_id)
33
- if app[:state] == Enums::ApplicationStates::CREATED
34
- fail Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be stopped'
35
- end
36
-
37
- scale_worker(application_id, 0)
38
- update_application(application_id, maintenance: true)
39
- end
40
-
41
- # @see Stub#restart
42
- def restart(application_id)
43
- log.debug "Restart @ #{@endpoint_url}"
44
- stop(application_id)
45
- start(application_id)
46
- end
47
- end
48
- end
49
- end
50
- end
51
- end
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Lifecycle
6
+ # Lifecycle:
7
+ # A) via maintenance - workers are still active
8
+ # B) via formation - loose scaling information
9
+
10
+ # @see Stub#start
11
+ def start(application_id)
12
+ log.debug "Start @ #{@endpoint_url}"
13
+
14
+ app = application(application_id)
15
+ if app[:state] == Enums::ApplicationStates::DEPLOYED
16
+ # add web dyno if there currently are no dynos (state == deployed)
17
+ scale_web(application_id, 1)
18
+ elsif app[:state] == Enums::ApplicationStates::CREATED
19
+ # fail if there is no deployment
20
+ raise Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be started'
21
+ end
22
+
23
+ scale_worker(application_id, 1)
24
+ update_application(application_id, maintenance: false)
25
+ end
26
+
27
+ # @see Stub#stop
28
+ def stop(application_id)
29
+ log.debug "Stop @ #{@endpoint_url}"
30
+
31
+ # fail if there is no deployment
32
+ app = application(application_id)
33
+ if app[:state] == Enums::ApplicationStates::CREATED
34
+ raise Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be stopped'
35
+ end
36
+
37
+ scale_worker(application_id, 0)
38
+ update_application(application_id, maintenance: true)
39
+ end
40
+
41
+ # @see Stub#restart
42
+ def restart(application_id)
43
+ log.debug "Restart @ #{@endpoint_url}"
44
+ stop(application_id)
45
+ start(application_id)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -33,8 +33,8 @@ module Nucleus
33
33
  # @see Stub#log_entries
34
34
  def log_entries(application_id, log_id)
35
35
  unless log?(application_id, log_id)
36
- fail Errors::AdapterResourceNotFoundError,
37
- "Invalid log file '#{log_id}', not available for application '#{application_id}'"
36
+ raise Errors::AdapterResourceNotFoundError,
37
+ "Invalid log file '#{log_id}', not available for application '#{application_id}'"
38
38
  end
39
39
 
40
40
  return build_log_entries(application_id) if log_id.to_sym == Enums::ApplicationLogfileType::BUILD
@@ -1,42 +1,42 @@
1
- module Nucleus
2
- module Adapters
3
- module V1
4
- class Heroku < Stub
5
- module Regions
6
- # @see Stub#regions
7
- def regions
8
- response = get('/regions').body
9
- response.each do |region|
10
- region[:id] = region.delete(:name).upcase
11
- end
12
- response
13
- end
14
-
15
- # @see Stub#region
16
- def region(region_name)
17
- found_region = native_region(region_name)
18
- fail Errors::AdapterResourceNotFoundError,
19
- "Region '#{region_name}' does not exist at the endpoint" if found_region.nil?
20
- found_region[:id] = found_region.delete(:name).upcase
21
- found_region
22
- end
23
-
24
- private
25
-
26
- def retrieve_region(application)
27
- return unless application.key?(:region)
28
- found_region = native_region(application[:region])
29
- fail Errors::SemanticAdapterRequestError,
30
- "Region '#{application[:region]}' does not exist at the endpoint" if found_region.nil?
31
- application[:region] = found_region[:id]
32
- end
33
-
34
- def native_region(region_name)
35
- response = get('/regions').body
36
- response.find { |region| region[:name].casecmp(region_name) == 0 }
37
- end
38
- end
39
- end
40
- end
41
- end
42
- end
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Regions
6
+ # @see Stub#regions
7
+ def regions
8
+ response = get('/regions').body
9
+ response.each do |region|
10
+ region[:id] = region.delete(:name).upcase
11
+ end
12
+ response
13
+ end
14
+
15
+ # @see Stub#region
16
+ def region(region_name)
17
+ found_region = native_region(region_name)
18
+ raise Errors::AdapterResourceNotFoundError,
19
+ "Region '#{region_name}' does not exist at the endpoint" if found_region.nil?
20
+ found_region[:id] = found_region.delete(:name).upcase
21
+ found_region
22
+ end
23
+
24
+ private
25
+
26
+ def retrieve_region(application)
27
+ return unless application.key?(:region)
28
+ found_region = native_region(application[:region])
29
+ raise Errors::SemanticAdapterRequestError,
30
+ "Region '#{application[:region]}' does not exist at the endpoint" if found_region.nil?
31
+ application[:region] = found_region[:id]
32
+ end
33
+
34
+ def native_region(region_name)
35
+ response = get('/regions').body
36
+ response.find { |region| region[:name].casecmp(region_name) == 0 }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,168 +1,168 @@
1
- module Nucleus
2
- module Adapters
3
- module V1
4
- class Heroku < Stub
5
- module Services
6
- # @see Stub#services
7
- def services
8
- get('/addon-services').body.collect { |service| to_nucleus_service(service) }
9
- end
10
-
11
- # @see Stub#service
12
- def service(service_id)
13
- to_nucleus_service(get("/addon-services/#{service_id}").body)
14
- end
15
-
16
- # @see Stub#service_plans
17
- def service_plans(service_id)
18
- load_plans(service_id).collect { |plan| to_nucleus_plan(plan) }.sort_by do |plan|
19
- # only compare the first cost, covers most cases and sorting for all costs would be far too complex
20
- plan[:costs][0][:price].first[:amount].to_f
21
- end
22
- end
23
-
24
- # @see Stub#service_plan
25
- def service_plan(service_id, plan_id)
26
- to_nucleus_plan(get("/addon-services/#{service_id}/plans/#{plan_id}").body)
27
- end
28
-
29
- # @see Stub#installed_services
30
- def installed_services(application_id)
31
- get("/apps/#{application_id}/addons").body.collect { |service| to_nucleus_installed_service(service) }
32
- end
33
-
34
- # @see Stub#installed_service
35
- def installed_service(application_id, service_id)
36
- assigned_service = raw_installed_service(application_id, service_id)
37
- to_nucleus_installed_service(assigned_service)
38
- end
39
-
40
- # @see Stub#add_service
41
- def add_service(application_id, service_entity, plan_entity)
42
- begin
43
- # make sure plan belongs to this service, throws 404 if no such plan
44
- # the service plan itself requires the name, e.g. 'sandbox' or the UUID
45
- service_plan(service_entity[:id], plan_entity[:id])
46
- rescue Errors::AdapterResourceNotFoundError => e
47
- # convert to 422
48
- raise Errors::SemanticAdapterRequestError, e.message
49
- end
50
- # the plan to choose requires the UUID of the plan OR the combination of both names
51
- plan_id = service_plan_identifier(service_entity[:id], plan_entity[:id])
52
- created = post("/apps/#{application_id}/addons", body: { plan: plan_id }).body
53
- to_nucleus_installed_service(created)
54
- end
55
-
56
- # @see Stub#change_service
57
- def change_service(application_id, service_id, plan_entity)
58
- # make sure service is bound to the application
59
- assignment_id = raw_installed_service(application_id, service_id)[:id]
60
- begin
61
- # make sure plan belongs to this service, throws 404 if no such plan
62
- # the service plan itself requires the name, e.g. 'sandbox' or the UUID
63
- service_plan(service_id, plan_entity[:id])
64
- rescue Errors::AdapterResourceNotFoundError => e
65
- # convert to 422
66
- raise Errors::SemanticAdapterRequestError, e.message
67
- end
68
- # the plan to choose requires the UUID of the plan OR the combination of both names
69
- plan_id = service_plan_identifier(service_id, plan_id)
70
- updated = patch("/apps/#{application_id}/addons/#{assignment_id}", body: { plan: plan_id }).body
71
- to_nucleus_installed_service(updated)
72
- end
73
-
74
- # @see Stub#remove_service
75
- def remove_service(application_id, service_id)
76
- # make sure service is bound to the application
77
- assignment_id = raw_installed_service(application_id, service_id)[:id]
78
- delete("/apps/#{application_id}/addons/#{assignment_id}")
79
- end
80
-
81
- private
82
-
83
- def service_plan_identifier(service_id, plan_id)
84
- # process plan id_or_name to build the unique identifier
85
- # a) is a UUID
86
- return plan_id if Regexp::UUID_PATTERN =~ plan_id
87
- # b) is valid identifier, contains ':'
88
- return plan_id if /^[-\w]+:[-\w]+$/i =~ plan_id
89
- # c) fetch id for name
90
- return "#{service_id}:#{plan_id}" unless Regexp::UUID_PATTERN =~ service_id
91
- # arriving here, service_id is UUID but plan_id is the name --> DOH!
92
- # we return the plan_id and the request will presumably fail
93
- plan_id
94
- end
95
-
96
- def raw_installed_service(application_id, service_id)
97
- # here we probably receive the ID of the service, not the service assignment ID itself
98
- installed = get("/apps/#{application_id}/addons/#{service_id}", expects: [200, 404])
99
- if installed.status == 404
100
- assignment_id = service_assignment_id(application_id, service_id)
101
- fail Errors::AdapterResourceNotFoundError,
102
- "Service #{service_id} is not assigned to application #{application_id}" unless assignment_id
103
- return get("/apps/#{application_id}/addons/#{assignment_id}").body
104
- end
105
- installed.body
106
- end
107
-
108
- def service_assignment_id(application_id, service_id)
109
- all_services = get("/apps/#{application_id}/addons").body
110
- match = all_services.find do |addon|
111
- addon[:addon_service][:id] == service_id || addon[:addon_service][:name] == service_id
112
- end
113
- return match[:id] if match
114
- nil
115
- end
116
-
117
- def to_nucleus_service(service)
118
- service[:description] = service.delete(:human_name)
119
- service[:release] = service.delete(:state)
120
- service[:required_services] = []
121
- service[:free_plan] = free_plan?(service[:id])
122
- service[:documentation_url] = "https://addons.heroku.com/#{service[:name]}"
123
- service
124
- end
125
-
126
- def to_nucleus_installed_service(installed_service)
127
- service = service(installed_service[:addon_service][:id])
128
- # get all variables and reject all that do not belong to the addon
129
- unless installed_service[:config_vars].nil? && installed_service[:config_vars].empty?
130
- vars = get("/apps/#{installed_service[:app][:id]}/config-vars").body
131
- # ignore all vars that do not belong to the service
132
- vars = vars.delete_if { |k| !installed_service[:config_vars].include?(k.to_s) }
133
- # format to desired format
134
- vars = vars.collect { |k, v| { key: k, value: v, description: nil } }
135
- end
136
- service[:properties] = vars ? vars : []
137
- service[:active_plan] = installed_service[:plan][:id]
138
- service[:web_url] = installed_service[:web_url]
139
- service
140
- end
141
-
142
- def to_nucleus_plan(plan)
143
- # TODO: extract payment period to enum
144
- plan[:costs] = [{ price: [amount: plan[:price][:cents] / 100.0, currency: 'USD'],
145
- period: plan[:price][:unit], per_instance: false }]
146
- plan[:free] = plan[:price][:cents] == 0
147
- plan
148
- end
149
-
150
- def load_plans(service_id)
151
- get("/addon-services/#{service_id}/plans").body
152
- end
153
-
154
- # Memoize this detection.
155
- # The information is not critical, but takes some time to evaluate.
156
- # Values are not expected to change often.
157
- def free_plan?(service_id, plans = nil)
158
- @free_plans ||= {}
159
- return @free_plans[service_id] if @free_plans.key?(service_id)
160
- plans = load_plans(service_id) unless plans
161
- @free_plans[service_id] = plans.any? { |plan| plan[:price][:cents] == 0 }
162
- @free_plans[service_id]
163
- end
164
- end
165
- end
166
- end
167
- end
168
- end
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Services
6
+ # @see Stub#services
7
+ def services
8
+ get('/addon-services').body.collect { |service| to_nucleus_service(service) }
9
+ end
10
+
11
+ # @see Stub#service
12
+ def service(service_id)
13
+ to_nucleus_service(get("/addon-services/#{service_id}").body)
14
+ end
15
+
16
+ # @see Stub#service_plans
17
+ def service_plans(service_id)
18
+ load_plans(service_id).collect { |plan| to_nucleus_plan(plan) }.sort_by do |plan|
19
+ # only compare the first cost, covers most cases and sorting for all costs would be far too complex
20
+ plan[:costs][0][:price].first[:amount].to_f
21
+ end
22
+ end
23
+
24
+ # @see Stub#service_plan
25
+ def service_plan(service_id, plan_id)
26
+ to_nucleus_plan(get("/addon-services/#{service_id}/plans/#{plan_id}").body)
27
+ end
28
+
29
+ # @see Stub#installed_services
30
+ def installed_services(application_id)
31
+ get("/apps/#{application_id}/addons").body.collect { |service| to_nucleus_installed_service(service) }
32
+ end
33
+
34
+ # @see Stub#installed_service
35
+ def installed_service(application_id, service_id)
36
+ assigned_service = raw_installed_service(application_id, service_id)
37
+ to_nucleus_installed_service(assigned_service)
38
+ end
39
+
40
+ # @see Stub#add_service
41
+ def add_service(application_id, service_entity, plan_entity)
42
+ begin
43
+ # make sure plan belongs to this service, throws 404 if no such plan
44
+ # the service plan itself requires the name, e.g. 'sandbox' or the UUID
45
+ service_plan(service_entity[:id], plan_entity[:id])
46
+ rescue Errors::AdapterResourceNotFoundError => e
47
+ # convert to 422
48
+ raise Errors::SemanticAdapterRequestError, e.message
49
+ end
50
+ # the plan to choose requires the UUID of the plan OR the combination of both names
51
+ plan_id = service_plan_identifier(service_entity[:id], plan_entity[:id])
52
+ created = post("/apps/#{application_id}/addons", body: { plan: plan_id }).body
53
+ to_nucleus_installed_service(created)
54
+ end
55
+
56
+ # @see Stub#change_service
57
+ def change_service(application_id, service_id, plan_entity)
58
+ # make sure service is bound to the application
59
+ assignment_id = raw_installed_service(application_id, service_id)[:id]
60
+ begin
61
+ # make sure plan belongs to this service, throws 404 if no such plan
62
+ # the service plan itself requires the name, e.g. 'sandbox' or the UUID
63
+ service_plan(service_id, plan_entity[:id])
64
+ rescue Errors::AdapterResourceNotFoundError => e
65
+ # convert to 422
66
+ raise Errors::SemanticAdapterRequestError, e.message
67
+ end
68
+ # the plan to choose requires the UUID of the plan OR the combination of both names
69
+ plan_id = service_plan_identifier(service_id, plan_id)
70
+ updated = patch("/apps/#{application_id}/addons/#{assignment_id}", body: { plan: plan_id }).body
71
+ to_nucleus_installed_service(updated)
72
+ end
73
+
74
+ # @see Stub#remove_service
75
+ def remove_service(application_id, service_id)
76
+ # make sure service is bound to the application
77
+ assignment_id = raw_installed_service(application_id, service_id)[:id]
78
+ delete("/apps/#{application_id}/addons/#{assignment_id}")
79
+ end
80
+
81
+ private
82
+
83
+ def service_plan_identifier(service_id, plan_id)
84
+ # process plan id_or_name to build the unique identifier
85
+ # a) is a UUID
86
+ return plan_id if Regexp::UUID_PATTERN =~ plan_id
87
+ # b) is valid identifier, contains ':'
88
+ return plan_id if /^[-\w]+:[-\w]+$/i =~ plan_id
89
+ # c) fetch id for name
90
+ return "#{service_id}:#{plan_id}" unless Regexp::UUID_PATTERN =~ service_id
91
+ # arriving here, service_id is UUID but plan_id is the name --> DOH!
92
+ # we return the plan_id and the request will presumably fail
93
+ plan_id
94
+ end
95
+
96
+ def raw_installed_service(application_id, service_id)
97
+ # here we probably receive the ID of the service, not the service assignment ID itself
98
+ installed = get("/apps/#{application_id}/addons/#{service_id}", expects: [200, 404])
99
+ if installed.status == 404
100
+ assignment_id = service_assignment_id(application_id, service_id)
101
+ raise Errors::AdapterResourceNotFoundError,
102
+ "Service #{service_id} is not assigned to application #{application_id}" unless assignment_id
103
+ return get("/apps/#{application_id}/addons/#{assignment_id}").body
104
+ end
105
+ installed.body
106
+ end
107
+
108
+ def service_assignment_id(application_id, service_id)
109
+ all_services = get("/apps/#{application_id}/addons").body
110
+ match = all_services.find do |addon|
111
+ addon[:addon_service][:id] == service_id || addon[:addon_service][:name] == service_id
112
+ end
113
+ return match[:id] if match
114
+ nil
115
+ end
116
+
117
+ def to_nucleus_service(service)
118
+ service[:description] = service.delete(:human_name)
119
+ service[:release] = service.delete(:state)
120
+ service[:required_services] = []
121
+ service[:free_plan] = free_plan?(service[:id])
122
+ service[:documentation_url] = "https://addons.heroku.com/#{service[:name]}"
123
+ service
124
+ end
125
+
126
+ def to_nucleus_installed_service(installed_service)
127
+ service = service(installed_service[:addon_service][:id])
128
+ # get all variables and reject all that do not belong to the addon
129
+ unless installed_service[:config_vars].nil? && installed_service[:config_vars].empty?
130
+ vars = get("/apps/#{installed_service[:app][:id]}/config-vars").body
131
+ # ignore all vars that do not belong to the service
132
+ vars = vars.delete_if { |k| !installed_service[:config_vars].include?(k.to_s) }
133
+ # format to desired format
134
+ vars = vars.collect { |k, v| { key: k, value: v, description: nil } }
135
+ end
136
+ service[:properties] = vars ? vars : []
137
+ service[:active_plan] = installed_service[:plan][:id]
138
+ service[:web_url] = installed_service[:web_url]
139
+ service
140
+ end
141
+
142
+ def to_nucleus_plan(plan)
143
+ # TODO: extract payment period to enum
144
+ plan[:costs] = [{ price: [amount: plan[:price][:cents] / 100.0, currency: 'USD'],
145
+ period: plan[:price][:unit], per_instance: false }]
146
+ plan[:free] = plan[:price][:cents] == 0
147
+ plan
148
+ end
149
+
150
+ def load_plans(service_id)
151
+ get("/addon-services/#{service_id}/plans").body
152
+ end
153
+
154
+ # Memoize this detection.
155
+ # The information is not critical, but takes some time to evaluate.
156
+ # Values are not expected to change often.
157
+ def free_plan?(service_id, plans = nil)
158
+ @free_plans ||= {}
159
+ return @free_plans[service_id] if @free_plans.key?(service_id)
160
+ plans = load_plans(service_id) unless plans
161
+ @free_plans[service_id] = plans.any? { |plan| plan[:price][:cents] == 0 }
162
+ @free_plans[service_id]
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end