nucleus 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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