├── .bundle
└── config
├── .gitignore
├── .rspec
├── .ruby-gemset
├── .ruby-version
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── NOTICE
├── README.md
├── Rakefile
├── app
├── assets
│ ├── images
│ │ ├── cf-mysql-logo.png
│ │ └── favicon.ico
│ ├── javascripts
│ │ └── application.js
│ └── stylesheets
│ │ ├── application.css.scss
│ │ ├── cf-mysql-broker.css
│ │ └── pivotal-styles-full.css
├── controllers
│ ├── application_controller.rb
│ ├── concerns
│ │ └── .keep
│ ├── manage
│ │ ├── auth_controller.rb
│ │ └── instances_controller.rb
│ ├── previews_controller.rb
│ └── v2
│ │ ├── base_controller.rb
│ │ ├── catalogs_controller.rb
│ │ ├── service_bindings_controller.rb
│ │ └── service_instances_controller.rb
├── models
│ ├── base_model.rb
│ ├── catalog.rb
│ ├── cloud_controller_http_client.rb
│ ├── concerns
│ │ └── .keep
│ ├── database.rb
│ ├── plan.rb
│ ├── read_only_user.rb
│ ├── service.rb
│ ├── service_binding.rb
│ ├── service_instance.rb
│ └── service_instance_access_verifier.rb
├── queries
│ └── service_instance_usage_query.rb
└── views
│ ├── errors
│ ├── approvals_error.html.erb
│ └── not_authorized.html.erb
│ ├── layouts
│ └── application.html.erb
│ └── manage
│ └── instances
│ └── show.html.erb
├── bin
├── bundle
├── rails
└── rake
├── config.ru
├── config
├── application.rb
├── boot.rb
├── database.yml
├── environment.rb
├── environments
│ ├── assets.rb
│ ├── development.rb
│ ├── production.rb
│ ├── test.rb
│ └── travis_test.rb
├── initializers
│ ├── cookies_serializer.rb
│ ├── omniauth.rb
│ ├── session_store.rb
│ └── wrap_parameters.rb
├── routes.rb
├── secrets.yml
└── settings.yml
├── db
├── migrate
│ ├── 20140529214059_create_service_instances.rb
│ ├── 20140616163621_add_quota_to_service_instances.rb
│ ├── 20140616212709_add_db_name_to_service_instances.rb
│ ├── 20140918004518_set_db_name.rb
│ └── 20180117002358_create_read_only_users.rb
└── schema.rb
├── lib
├── configuration.rb
├── request_response_logger.rb
├── service_capacity.rb
├── service_instance_manager.rb
├── settings.rb
├── table_lock_manager.rb
├── tasks
│ ├── brakeman.rake
│ ├── broker.rake
│ └── table_locks.rake
└── uaa_session.rb
├── log
└── .keep
├── manifest.yml
└── spec
├── controllers
├── manage
│ ├── auth_controller_spec.rb
│ └── instances_controller_spec.rb
├── previews_controller_spec.rb
└── v2
│ ├── catalogs_controller_spec.rb
│ ├── service_bindings_controller_spec.rb
│ └── service_instances_controller_spec.rb
├── features
└── plan_update_spec.rb
├── lib
├── configuration_spec.rb
├── request_response_logger_spec.rb
├── service_capacity_spec.rb
├── service_instance_manager_spec.rb
├── table_lock_manager_spec.rb
└── uaa_session_spec.rb
├── models
├── catalog_spec.rb
├── cloud_controller_http_client_spec.rb
├── database_spec.rb
├── plan_spec.rb
├── service_binding_spec.rb
├── service_instance_access_verifier_spec.rb
├── service_instance_spec.rb
└── service_spec.rb
├── queries
└── service_instance_usage_query_spec.rb
├── requests
├── catalog_spec.rb
├── lifecycle_spec.rb
└── missing_things_spec.rb
├── spec_helper.rb
└── support
├── controller_helpers.rb
├── feature_helpers.rb
├── model_helpers.rb
├── mysql_helper.rb
└── request_helpers.rb
/.bundle/config:
--------------------------------------------------------------------------------
1 | ---
2 | BUNDLE_CACHE_ALL: "true"
3 | BUNDLE_PATH: "vendor/bundle"
4 | BUNDLE_DISABLE_SHARED_GEMS: "true"
5 | BUNDLE_WITHOUT: "development"
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore the default SQLite database.
8 | /db/*.sqlite3
9 | /db/*.sqlite3-journal
10 |
11 | # Ignore all logfiles and tempfiles.
12 | /log/*.log
13 | /tmp
14 |
15 | vendor/
16 |
17 | .idea
18 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | cf-mysql-broker
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.3.8
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | ruby '~> 2.3'
4 |
5 | gem 'rails', '~> 4.2'
6 | gem 'rails-api'
7 | gem 'jquery-rails'
8 | gem 'settingslogic'
9 | gem 'mysql2', '< 0.5'
10 | gem 'omniauth-uaa-oauth2', git: 'https://github.com/cloudfoundry/omniauth-uaa-oauth2'
11 | gem 'nats'
12 | gem 'sass-rails'
13 | gem 'eventmachine', '~> 1.0.7'
14 |
15 | group :production do
16 | gem 'unicorn'
17 | end
18 |
19 | group :development, :test do
20 | gem 'test-unit'
21 | gem 'rspec-rails'
22 | gem 'database_cleaner'
23 | gem 'brakeman'
24 | gem 'pry'
25 | gem 'rb-readline'
26 | end
27 |
28 | group :test do
29 | gem 'codeclimate-test-reporter', '~> 0.6.0', require: nil
30 | gem 'webmock'
31 | end
32 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/cloudfoundry/omniauth-uaa-oauth2
3 | revision: 1eaf9b3f36f98a51a5453c94175d8b265d46bb56
4 | specs:
5 | omniauth-uaa-oauth2 (1.0.0)
6 | cf-uaa-lib (>= 3.2, < 4.0)
7 | omniauth (~> 1.0)
8 |
9 | GEM
10 | remote: https://rubygems.org/
11 | specs:
12 | actionmailer (4.2.11.1)
13 | actionpack (= 4.2.11.1)
14 | actionview (= 4.2.11.1)
15 | activejob (= 4.2.11.1)
16 | mail (~> 2.5, >= 2.5.4)
17 | rails-dom-testing (~> 1.0, >= 1.0.5)
18 | actionpack (4.2.11.1)
19 | actionview (= 4.2.11.1)
20 | activesupport (= 4.2.11.1)
21 | rack (~> 1.6)
22 | rack-test (~> 0.6.2)
23 | rails-dom-testing (~> 1.0, >= 1.0.5)
24 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
25 | actionview (4.2.11.1)
26 | activesupport (= 4.2.11.1)
27 | builder (~> 3.1)
28 | erubis (~> 2.7.0)
29 | rails-dom-testing (~> 1.0, >= 1.0.5)
30 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
31 | activejob (4.2.11.1)
32 | activesupport (= 4.2.11.1)
33 | globalid (>= 0.3.0)
34 | activemodel (4.2.11.1)
35 | activesupport (= 4.2.11.1)
36 | builder (~> 3.1)
37 | activerecord (4.2.11.1)
38 | activemodel (= 4.2.11.1)
39 | activesupport (= 4.2.11.1)
40 | arel (~> 6.0)
41 | activesupport (4.2.11.1)
42 | i18n (~> 0.7)
43 | minitest (~> 5.1)
44 | thread_safe (~> 0.3, >= 0.3.4)
45 | tzinfo (~> 1.1)
46 | addressable (2.6.0)
47 | public_suffix (>= 2.0.2, < 4.0)
48 | arel (6.0.4)
49 | brakeman (4.5.0)
50 | builder (3.2.3)
51 | cf-uaa-lib (3.14.3)
52 | httpclient (~> 2.8, >= 2.8.2.4)
53 | multi_json (~> 1.12.0, >= 1.12.1)
54 | codeclimate-test-reporter (0.6.0)
55 | simplecov (>= 0.7.1, < 1.0.0)
56 | coderay (1.1.2)
57 | concurrent-ruby (1.1.5)
58 | crack (0.4.3)
59 | safe_yaml (~> 1.0.0)
60 | crass (1.0.4)
61 | daemons (1.3.1)
62 | database_cleaner (1.7.0)
63 | diff-lcs (1.3)
64 | docile (1.3.1)
65 | erubis (2.7.0)
66 | eventmachine (1.0.7)
67 | ffi (1.10.0)
68 | globalid (0.4.2)
69 | activesupport (>= 4.2.0)
70 | hashdiff (0.3.8)
71 | hashie (3.6.0)
72 | httpclient (2.8.3)
73 | i18n (0.9.5)
74 | concurrent-ruby (~> 1.0)
75 | jquery-rails (4.3.3)
76 | rails-dom-testing (>= 1, < 3)
77 | railties (>= 4.2.0)
78 | thor (>= 0.14, < 2.0)
79 | json (2.2.0)
80 | json_pure (1.8.6)
81 | kgio (2.11.2)
82 | loofah (2.2.3)
83 | crass (~> 1.0.2)
84 | nokogiri (>= 1.5.9)
85 | mail (2.7.1)
86 | mini_mime (>= 0.1.1)
87 | method_source (0.9.2)
88 | mini_mime (1.0.1)
89 | mini_portile2 (2.4.0)
90 | minitest (5.11.3)
91 | multi_json (1.12.2)
92 | mysql2 (0.4.10)
93 | nats (0.6.0)
94 | daemons (~> 1.1, >= 1.2.2)
95 | eventmachine (~> 1.0, = 1.0.7)
96 | json_pure (~> 1.8, >= 1.8.1)
97 | thin (~> 1.6, >= 1.6.3)
98 | nokogiri (1.10.3)
99 | mini_portile2 (~> 2.4.0)
100 | omniauth (1.9.0)
101 | hashie (>= 3.4.6, < 3.7.0)
102 | rack (>= 1.6.2, < 3)
103 | power_assert (1.1.3)
104 | pry (0.12.2)
105 | coderay (~> 1.1.0)
106 | method_source (~> 0.9.0)
107 | public_suffix (3.0.3)
108 | rack (1.6.11)
109 | rack-test (0.6.3)
110 | rack (>= 1.0)
111 | rails (4.2.11.1)
112 | actionmailer (= 4.2.11.1)
113 | actionpack (= 4.2.11.1)
114 | actionview (= 4.2.11.1)
115 | activejob (= 4.2.11.1)
116 | activemodel (= 4.2.11.1)
117 | activerecord (= 4.2.11.1)
118 | activesupport (= 4.2.11.1)
119 | bundler (>= 1.3.0, < 2.0)
120 | railties (= 4.2.11.1)
121 | sprockets-rails
122 | rails-api (0.4.1)
123 | actionpack (>= 3.2.11)
124 | railties (>= 3.2.11)
125 | rails-deprecated_sanitizer (1.0.3)
126 | activesupport (>= 4.2.0.alpha)
127 | rails-dom-testing (1.0.9)
128 | activesupport (>= 4.2.0, < 5.0)
129 | nokogiri (~> 1.6)
130 | rails-deprecated_sanitizer (>= 1.0.1)
131 | rails-html-sanitizer (1.0.4)
132 | loofah (~> 2.2, >= 2.2.2)
133 | railties (4.2.11.1)
134 | actionpack (= 4.2.11.1)
135 | activesupport (= 4.2.11.1)
136 | rake (>= 0.8.7)
137 | thor (>= 0.18.1, < 2.0)
138 | raindrops (0.19.0)
139 | rake (12.3.2)
140 | rb-fsevent (0.10.3)
141 | rb-inotify (0.10.0)
142 | ffi (~> 1.0)
143 | rb-readline (0.5.5)
144 | rspec-core (3.8.0)
145 | rspec-support (~> 3.8.0)
146 | rspec-expectations (3.8.2)
147 | diff-lcs (>= 1.2.0, < 2.0)
148 | rspec-support (~> 3.8.0)
149 | rspec-mocks (3.8.0)
150 | diff-lcs (>= 1.2.0, < 2.0)
151 | rspec-support (~> 3.8.0)
152 | rspec-rails (3.8.2)
153 | actionpack (>= 3.0)
154 | activesupport (>= 3.0)
155 | railties (>= 3.0)
156 | rspec-core (~> 3.8.0)
157 | rspec-expectations (~> 3.8.0)
158 | rspec-mocks (~> 3.8.0)
159 | rspec-support (~> 3.8.0)
160 | rspec-support (3.8.0)
161 | safe_yaml (1.0.5)
162 | sass (3.7.3)
163 | sass-listen (~> 4.0.0)
164 | sass-listen (4.0.0)
165 | rb-fsevent (~> 0.9, >= 0.9.4)
166 | rb-inotify (~> 0.9, >= 0.9.7)
167 | sass-rails (5.0.7)
168 | railties (>= 4.0.0, < 6)
169 | sass (~> 3.1)
170 | sprockets (>= 2.8, < 4.0)
171 | sprockets-rails (>= 2.0, < 4.0)
172 | tilt (>= 1.1, < 3)
173 | settingslogic (2.0.9)
174 | simplecov (0.16.1)
175 | docile (~> 1.1)
176 | json (>= 1.8, < 3)
177 | simplecov-html (~> 0.10.0)
178 | simplecov-html (0.10.2)
179 | sprockets (3.7.2)
180 | concurrent-ruby (~> 1.0)
181 | rack (> 1, < 3)
182 | sprockets-rails (3.2.1)
183 | actionpack (>= 4.0)
184 | activesupport (>= 4.0)
185 | sprockets (>= 3.0.0)
186 | test-unit (3.3.0)
187 | power_assert
188 | thin (1.7.2)
189 | daemons (~> 1.0, >= 1.0.9)
190 | eventmachine (~> 1.0, >= 1.0.4)
191 | rack (>= 1, < 3)
192 | thor (0.20.3)
193 | thread_safe (0.3.6)
194 | tilt (2.0.9)
195 | tzinfo (1.2.5)
196 | thread_safe (~> 0.1)
197 | unicorn (5.5.0)
198 | kgio (~> 2.6)
199 | raindrops (~> 0.7)
200 | webmock (3.5.1)
201 | addressable (>= 2.3.6)
202 | crack (>= 0.3.2)
203 | hashdiff
204 |
205 | PLATFORMS
206 | ruby
207 |
208 | DEPENDENCIES
209 | brakeman
210 | codeclimate-test-reporter (~> 0.6.0)
211 | database_cleaner
212 | eventmachine (~> 1.0.7)
213 | jquery-rails
214 | mysql2 (< 0.5)
215 | nats
216 | omniauth-uaa-oauth2!
217 | pry
218 | rails (~> 4.2)
219 | rails-api
220 | rb-readline
221 | rspec-rails
222 | sass-rails
223 | settingslogic
224 | test-unit
225 | unicorn
226 | webmock
227 |
228 | RUBY VERSION
229 | ruby 2.3.1p112
230 |
231 | BUNDLED WITH
232 | 1.17.3
233 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2013-2015 Pivotal
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015-Present CloudFoundry.org Foundation, Inc. All Rights Reserved.
2 |
3 | This project contains software that is Copyright (c) 2013-2015 Pivotal Software, Inc.
4 |
5 | This project is licensed to you under the Apache License, Version 2.0 (the "License").
6 |
7 | You may not use this project except in compliance with the License.
8 |
9 | This project may include a number of subcomponents with separate copyright notices
10 | and license terms. Your use of these subcomponents is subject to the terms and
11 | conditions of the subcomponent's license, as noted in the LICENSE file.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # CF MySQL Broker
3 |
4 | CF MySQL Broker provides MySQL databases as a Cloud Foundry service. This broker demonstrates the v2 services API between cloud controllers and service brokers. This API is not to be confused with the cloud controller API; for more info see [Service Broker API](http://docs.cloudfoundry.org/services/api.html).
5 |
6 | The broker does not include a MySQL server. Instead, it is meant to be deployed alongside a MySQL server, which it manages, as part of [cf-mysql-release](https://github.com/cloudfoundry/cf-mysql-release). Deploying this as a standalone application to cloud foundry (e.g. via `cf push`) is not supported. The MySQL management tasks that the broker performs are as follows:
7 |
8 | * Provisioning of database instances (create)
9 | * Creation of credentials (bind)
10 | * Removal of credentials (unbind)
11 | * Unprovisioning of database instances (delete)
12 |
13 | ## Running Tests
14 |
15 | The CF MySQL Broker integration specs will exercise the catalog fetch, create, bind, unbind, and delete functions against its locally installed database.
16 |
17 | 1. Run a local install MySQL server.
18 | * Specs have only been tested with MySQL 5.6
19 | * Specs assume MySQL root user without a password
20 | * The anonymous user (User='' and Host='localhost') must be deleted (see [spec/spec_helper])
21 | - `delete from mysql.user where name='';`
22 | - `flush privileges`
23 | * Specific innodb settings are required (see [spec/spec_helper])
24 | - innodb_stats_on_metadata = ON
25 | - `mysql -uroot -e "SET GLOBAL innodb_stats_on_metadata=ON"`
26 | - innodb_stats_persistent = OFF
27 | - `mysql -uroot -e "SET GLOBAL innodb_stats_persistent=OFF"`
28 | 2. Run the following commands
29 |
30 | ```
31 | $ cd cf-mysql-broker
32 | $ bundle
33 | $ bundle exec rake db:setup
34 | $ bundle exec rake spec
35 | ```
36 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | CfMysqlBroker::Application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/images/cf-mysql-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/assets/images/cf-mysql-logo.png
--------------------------------------------------------------------------------
/app/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/assets/images/favicon.ico
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | //= require jquery
2 | //= require jquery_ujs
3 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css.scss:
--------------------------------------------------------------------------------
1 | @import "pivotal-styles-full";
2 | @import "cf-mysql-broker";
3 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/cf-mysql-broker.css:
--------------------------------------------------------------------------------
1 | .no-sidebar-main-wrapper {
2 | margin: 0 20px;
3 | left: 0;
4 | right: 0;
5 | width: auto;
6 | }
7 |
8 | .header {
9 | background-color: #243640;
10 | }
11 |
12 | .float-right {
13 | float: right;
14 | }
15 |
16 | .services-container {
17 | padding: 0 15px;
18 | }
19 |
20 | .header-container {
21 | color: #243640;
22 | font-size: 18px;
23 | margin-top: 10px;
24 | margin-bottom: 20px;
25 | }
26 |
27 | .teal-text {
28 | color: #00a79d;
29 | font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
30 | font-size: 13px;
31 | }
32 |
33 | .panel-text {
34 | font-size: 16px;
35 | }
36 |
37 | .panel-message {
38 | max-width: 600px;
39 | background-color: #f6f6f6;
40 | border: none;
41 | border-bottom: 4px solid rgba(0, 0, 0, 0.07);
42 | min-height: 88px;
43 | -moz-background-clip: padding;
44 | /* Firefox 3.6 */
45 | -webkit-background-clip: padding;
46 | /* Safari 4? Chrome 6? */
47 | background-clip: padding-box;
48 | /* Firefox 4, Safari 5, Opera 10, IE 9 */
49 | }
50 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/manage/auth_controller.rb:
--------------------------------------------------------------------------------
1 | module Manage
2 | class AuthController < ApplicationController
3 | protect_from_forgery with: :exception, only: :destroy
4 |
5 | def create
6 | auth = request.env['omniauth.auth'].to_hash
7 | credentials = auth['credentials']
8 |
9 | token = credentials['token']
10 | if token.empty?
11 | return render 'errors/approvals_error'
12 | end
13 |
14 | raw_info = auth['extra']['raw_info']
15 | unless raw_info
16 | return render 'errors/approvals_error'
17 | end
18 |
19 | session[:uaa_user_id] = auth['extra']['raw_info']['user_id']
20 | session[:uaa_access_token] = credentials['token']
21 | session[:uaa_refresh_token] = credentials['refresh_token']
22 | session[:last_seen] = Time.now
23 |
24 | redirect_to manage_instance_path(session[:instance_id])
25 | end
26 |
27 | def failure
28 | render text: message_param[:message], status: 403
29 | end
30 |
31 | def destroy
32 | session.clear
33 | redirect_to ::Configuration.auth_server_logout_url
34 | end
35 |
36 | private
37 |
38 | def message_param
39 | params.permit(:message)
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/controllers/manage/instances_controller.rb:
--------------------------------------------------------------------------------
1 | module Manage
2 | class InstancesController < ApplicationController
3 |
4 | before_filter :redirect_ssl
5 | before_filter :require_login
6 | before_filter :build_uaa_session
7 | before_filter :ensure_all_necessary_scopes_are_approved
8 | before_filter :ensure_can_manage_instance
9 |
10 | def show
11 | instance = ServiceInstance.find_by_guid(params[:id])
12 |
13 | @used_data = ServiceInstanceUsageQuery.new(instance).execute
14 | @quota = instance.max_storage_mb
15 | @over_quota = @used_data > @quota
16 | end
17 |
18 | private
19 |
20 | def redirect_ssl
21 | redirect_to :protocol => "https://" if Settings.ssl_enabled && request.protocol == 'http://'
22 | return true
23 | end
24 |
25 | def require_login
26 | session[:instance_id] = params[:id]
27 | unless logged_in?
28 | redirect_to '/manage/auth/cloudfoundry'
29 | return false
30 | end
31 | end
32 |
33 | def build_uaa_session
34 | @uaa_session = UaaSession.build(session[:uaa_access_token], session[:uaa_refresh_token])
35 | session[:uaa_access_token] = @uaa_session.access_token
36 | session[:uaa_refresh_token] = @uaa_session.refresh_token
37 | end
38 |
39 | def ensure_all_necessary_scopes_are_approved
40 | token_hash = CF::UAA::TokenCoder.decode(@uaa_session.access_token, verify: false)
41 | return true if has_necessary_scopes?(token_hash)
42 |
43 | if need_to_retry?
44 | session[:has_retried] = 'true'
45 | redirect_to '/manage/auth/cloudfoundry'
46 | return false
47 | else
48 | session[:has_retried] = 'false'
49 | render 'errors/approvals_error'
50 | return false
51 | end
52 | end
53 |
54 | def ensure_can_manage_instance
55 | cc_client = CloudControllerHttpClient.new(@uaa_session.auth_header)
56 | unless ServiceInstanceAccessVerifier.can_manage_instance?(params[:id], cc_client)
57 | render 'errors/not_authorized'
58 | return false
59 | end
60 | end
61 |
62 | def logged_in?
63 | oldest_allowable_last_seen_time = Time.now - Settings.session_expiry
64 |
65 | if session[:uaa_user_id].present? && (session[:last_seen] > oldest_allowable_last_seen_time)
66 | session[:last_seen] = Time.now
67 | return true
68 | end
69 |
70 | return false
71 | end
72 |
73 | def has_necessary_scopes?(token_hash)
74 | %w(openid cloud_controller_service_permissions.read).all? { |scope| token_hash['scope'].include?(scope) }
75 | end
76 |
77 | def need_to_retry?
78 | session[:has_retried].nil? || session[:has_retried] == 'false'
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/app/controllers/previews_controller.rb:
--------------------------------------------------------------------------------
1 | class PreviewsController < ApplicationController
2 | before_filter :restrict_to_development
3 |
4 | def show
5 | @quota = 100
6 | @used_data = 101
7 |
8 | @over_quota = @used_data > @quota
9 |
10 | render 'manage/instances/show'
11 | end
12 |
13 | private
14 |
15 | def restrict_to_development
16 | head(:bad_request) unless Rails.env.development?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/v2/base_controller.rb:
--------------------------------------------------------------------------------
1 | class V2::BaseController < ActionController::API
2 | include ActionController::HttpAuthentication::Basic::ControllerMethods
3 |
4 | before_filter :authenticate
5 |
6 | protected
7 |
8 | def authenticate
9 | authenticate_or_request_with_http_basic do |username, password|
10 | username == Settings.auth_username &&
11 | password == Settings.auth_password
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/v2/catalogs_controller.rb:
--------------------------------------------------------------------------------
1 | class V2::CatalogsController < V2::BaseController
2 | def show
3 | render json: {
4 | services: services.map {|service| service.to_hash }
5 | }
6 | end
7 |
8 | private
9 |
10 | def services
11 | (Settings['services'] || []).map {|attrs| Service.build(attrs)}
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/controllers/v2/service_bindings_controller.rb:
--------------------------------------------------------------------------------
1 | class V2::ServiceBindingsController < V2::BaseController
2 | ALLOWED_BINDING_PARAMETERS = ['read-only']
3 |
4 | def update
5 | instance = ServiceInstance.find_by_guid(params.fetch(:service_instance_id))
6 | if instance.nil?
7 | render status: 404, json: {}
8 | return
9 | end
10 |
11 | binding_parameters = params.fetch(:parameters, {})
12 | binding_parameters_include_unknown_key = binding_parameters.keys.any? {|key| !ALLOWED_BINDING_PARAMETERS.include?(key)}
13 |
14 | read_only = binding_parameters.fetch('read-only', false)
15 | read_only_parameter_has_invalid_value = !read_only.in?([true, false])
16 |
17 | if binding_parameters_include_unknown_key || read_only_parameter_has_invalid_value
18 | render status: 400, json: {
19 | "error" => "Error creating service binding",
20 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.",
21 | }
22 | return
23 | end
24 |
25 | binding = ServiceBinding.new(id: params.fetch(:id), service_instance: instance, read_only: read_only)
26 | binding.save
27 |
28 | render status: 201, json: binding
29 | end
30 |
31 | def destroy
32 | binding = ServiceBinding.find_by_id(params.fetch(:id))
33 | if binding
34 | binding.destroy
35 | status = 200
36 | else
37 | status = 410
38 | end
39 |
40 | render status: status, json: {}
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/controllers/v2/service_instances_controller.rb:
--------------------------------------------------------------------------------
1 | class V2::ServiceInstancesController < V2::BaseController
2 |
3 | # This is actually the create
4 | def update
5 | plan_guid = params.fetch(:plan_id)
6 |
7 | unless Catalog.has_plan?(plan_guid)
8 | return render status: 422, json: {'description' => "Cannot create a service instance. Plan #{plan_guid} was not found in the catalog."}
9 | end
10 |
11 | plan_max_storage_mb = Catalog.storage_quota_for_plan_guid(plan_guid)
12 |
13 | if ServiceCapacity.can_allocate?(plan_max_storage_mb)
14 | instance_guid = params.fetch(:id)
15 | instance = ServiceInstanceManager.create(guid: instance_guid, plan_guid: plan_guid)
16 |
17 | render status: 201, json: { dashboard_url: build_dashboard_url(instance) }
18 | else
19 | render status: 507, json: {'description' => 'Service capacity has been reached'}
20 | end
21 | end
22 |
23 | def set_plan
24 | instance_guid = params.fetch(:id)
25 | plan_guid = params.fetch(:plan_id)
26 |
27 | begin
28 | ServiceInstanceManager.set_plan(guid: instance_guid, plan_guid: plan_guid)
29 | status = 200
30 | body = {}
31 | rescue ServiceInstanceManager::ServiceInstanceNotFound
32 | status = 404
33 | body = { description: 'Service instance not found' }
34 | rescue ServiceInstanceManager::ServicePlanNotFound
35 | status = 400
36 | body = { description: 'Service plan not found' }
37 | rescue ServiceInstanceManager::InvalidServicePlanUpdate => e
38 | status = 422
39 | body = { description: e.message }
40 | end
41 |
42 | render status: status, json: body
43 | end
44 |
45 | def destroy
46 | instance_guid = params.fetch(:id)
47 | begin
48 | ServiceInstanceManager.destroy(guid: instance_guid)
49 | status = 200
50 | rescue ServiceInstanceManager::ServiceInstanceNotFound
51 | status = 410
52 | end
53 |
54 | render status: status, json: {}
55 | end
56 |
57 | private
58 |
59 | def build_dashboard_url(instance)
60 | domain = Settings.external_host
61 | path = manage_instance_path(instance.guid)
62 |
63 | "#{scheme}://#{domain}#{path}"
64 | end
65 |
66 | def scheme
67 | Settings['ssl_enabled'] == false ? 'http': 'https'
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/app/models/base_model.rb:
--------------------------------------------------------------------------------
1 | class BaseModel
2 | include ActiveModel::Model
3 |
4 | private
5 |
6 | def self.connection
7 | ActiveRecord::Base.connection
8 | end
9 |
10 | def connection
11 | self.class.connection
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/catalog.rb:
--------------------------------------------------------------------------------
1 | class Catalog
2 | def self.has_plan?(plan_guid)
3 | find_plan_by_guid(plan_guid).present?
4 | end
5 |
6 | def self.plans
7 | services.map do |service|
8 | service.plans
9 | end.flatten
10 | end
11 |
12 | def self.storage_quota_for_plan_guid(plan_guid)
13 | find_plan_by_guid(plan_guid).try(:max_storage_mb)
14 | end
15 |
16 | def self.connection_quota_for_plan_guid(plan_guid)
17 | find_plan_by_guid(plan_guid).try(:max_user_connections)
18 | end
19 |
20 | private
21 |
22 | def self.find_plan_by_guid(plan_guid)
23 | plans.detect do |plan|
24 | plan.id == plan_guid
25 | end
26 | end
27 |
28 | def self.services
29 | Settings['services'].map {|attrs| Service.build(attrs)}
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/models/cloud_controller_http_client.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 |
3 | class CloudControllerHttpClient
4 | attr_reader :auth_header
5 | def initialize(auth_header=nil)
6 | @auth_header = auth_header
7 | end
8 |
9 | def get(path)
10 | uri = cc_uri(path)
11 | http = build_http(uri)
12 |
13 | request = Net::HTTP::Get.new(uri)
14 | request['Authorization'] = auth_header
15 |
16 | response = http.request(request)
17 |
18 | JSON.parse(response.body)
19 | end
20 |
21 | private
22 |
23 | def cc_uri(path)
24 | URI.parse("#{Settings.cc_api_uri.gsub(/\/$/, '')}/#{path.gsub(/^\//, '')}")
25 | end
26 |
27 | def build_http(uri)
28 | http = Net::HTTP.new(uri.hostname, uri.port)
29 | http.use_ssl = uri.scheme == 'https'
30 | http.verify_mode = Settings.skip_ssl_validation ?
31 | OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
32 |
33 | http
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/database.rb:
--------------------------------------------------------------------------------
1 | module Database
2 | extend self
3 |
4 | def create(database_name)
5 | connection.execute("CREATE DATABASE IF NOT EXISTS #{connection.quote_table_name(database_name)}")
6 | end
7 |
8 | def drop(database_name)
9 | connection.execute("DROP DATABASE IF EXISTS #{connection.quote_table_name(database_name)}")
10 | end
11 |
12 | def exists?(database_name)
13 | connection.select_values("SHOW DATABASES LIKE '#{database_name}'").count > 0
14 | end
15 |
16 | def usage(database_name)
17 | res = connection.select_value(<<-SQL)
18 | SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1)
19 | FROM information_schema.tables AS tables
20 | WHERE tables.table_schema = '#{database_name}'
21 | SQL
22 |
23 | res.to_i
24 | end
25 |
26 | def with_reconnect
27 | yield
28 | rescue ActiveRecord::ActiveRecordError => e
29 | Rails.logger.warn(e)
30 | if ActiveRecord::Base.connection.active?
31 | raise e
32 | else
33 | reconnect_attempts = 0
34 | until ActiveRecord::Base.connection.active?
35 | reconnect_attempts += 1
36 | attempt_reconnect(reconnect_attempts)
37 | end
38 | end
39 | end
40 |
41 | private
42 |
43 | def attempt_reconnect(reconnect_attempts)
44 | Rails.logger.warn('No database connection, attempting to reconnect')
45 | ActiveRecord::Base.connection.reconnect!
46 | rescue Mysql2::Error => e
47 | Rails.logger.warn("Reconnect failed: #{e}")
48 |
49 | if reconnect_attempts < 20
50 | Kernel.sleep(3.seconds)
51 | else
52 | raise e
53 | end
54 | end
55 |
56 | def connection
57 | ActiveRecord::Base.connection
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/app/models/plan.rb:
--------------------------------------------------------------------------------
1 | class Plan
2 | attr_reader :id, :name, :description, :metadata, :max_storage_mb, :max_user_connections, :free
3 |
4 | def self.build(attrs)
5 | new(attrs)
6 | end
7 |
8 | def initialize(attrs)
9 | @id = attrs.fetch('id')
10 | @name = attrs.fetch('name')
11 | @description = attrs.fetch('description')
12 | @metadata = attrs.fetch('metadata', nil)
13 | @max_storage_mb = attrs.fetch('max_storage_mb', nil)
14 | @max_user_connections = attrs.fetch('max_user_connections', nil)
15 | @free = attrs.fetch('free',true)
16 | end
17 |
18 | def to_hash
19 | {
20 | 'id' => self.id,
21 | 'name' => self.name,
22 | 'description' => self.description,
23 | 'metadata' => self.metadata,
24 | 'max_storage_mb' => self.max_storage_mb,
25 | 'max_user_connections' => self.max_user_connections,
26 | 'free' => self.free,
27 | }
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/models/read_only_user.rb:
--------------------------------------------------------------------------------
1 | class ReadOnlyUser < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/service.rb:
--------------------------------------------------------------------------------
1 | class Service
2 | attr_reader :id, :name, :description, :tags, :metadata, :plans, :dashboard_client, :plan_updateable
3 |
4 | def self.build(attrs)
5 | plan_attrs = attrs['plans'] || []
6 | plans = plan_attrs.map { |attr| Plan.build(attr) }
7 | new(attrs.merge('plans' => plans))
8 | end
9 |
10 | def initialize(attrs)
11 | @id = attrs.fetch('id')
12 | @name = attrs.fetch('name')
13 | @description = attrs.fetch('description')
14 | @plan_updateable = attrs.fetch('plan_updateable', false)
15 | @tags = attrs.fetch('tags', [])
16 | @metadata = attrs.fetch('metadata', nil)
17 | @plans = attrs.fetch('plans', [])
18 | @dashboard_client = attrs.fetch('dashboard_client', {})
19 | end
20 |
21 | def bindable?
22 | true
23 | end
24 |
25 | def to_hash
26 | {
27 | 'id' => id,
28 | 'name' => name,
29 | 'description' => description,
30 | 'tags' => tags,
31 | 'metadata' => metadata,
32 | 'plan_updateable' => plan_updateable,
33 | 'plans' => plans.map(&:to_hash),
34 | 'bindable' => bindable?,
35 | 'dashboard_client' => dashboard_client
36 | }
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/models/service_binding.rb:
--------------------------------------------------------------------------------
1 | require Rails.root.join('lib/service_instance_manager')
2 |
3 | class DatabaseNotFoundError < StandardError; end
4 | class ServiceBinding < BaseModel
5 | attr_accessor :id, :service_instance, :read_only
6 |
7 | def self.find_by_id(id)
8 | binding = new(id: id)
9 |
10 | begin
11 | connection.execute("SHOW GRANTS FOR '#{binding.username}'")
12 | binding
13 | rescue ActiveRecord::StatementInvalid => e
14 | raise unless e.message =~ /no such grant/
15 | end
16 | end
17 |
18 |
19 | def self.find_by_id_and_service_instance_guid(id, instance_guid)
20 | binding = new(id: id)
21 |
22 | begin
23 | grants = connection.select_values("SHOW GRANTS FOR '#{binding.username}'")
24 |
25 | database_name = ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid)
26 | # Can we do this more elegantly, i.e., without checking for a
27 | # particular raw GRANT statement?
28 |
29 | if grants.any? { |grant| grant.match(Regexp.new("GRANT .* ON `#{database_name}`\\.\\* TO '#{binding.username}'@'%'")) }
30 | binding
31 | end
32 | rescue ActiveRecord::StatementInvalid => e
33 | raise unless e.message =~ /no such grant/
34 | end
35 | end
36 |
37 | def self.exists?(conditions)
38 | id = conditions.fetch(:id)
39 | instance_guid = conditions.fetch(:service_instance_guid)
40 |
41 | find_by_id_and_service_instance_guid(id, instance_guid).present?
42 | end
43 |
44 | def host
45 | connection_config.fetch('host')
46 | end
47 |
48 | def port
49 | connection_config.fetch('port')
50 | end
51 |
52 | def database_name
53 | ServiceInstanceManager.database_name_from_service_instance_guid(service_instance.guid)
54 | end
55 |
56 | def username
57 | Digest::MD5.base64digest(id).gsub(/[^a-zA-Z0-9]+/, '')[0...16]
58 | end
59 |
60 | def password
61 | @password ||= SecureRandom.base64(20).gsub(/[^a-zA-Z0-9]+/, '')[0...16]
62 | end
63 |
64 | def save
65 | unless Database.exists?(database_name)
66 | raise DatabaseNotFoundError.new("Service instance '#{service_instance.guid}' database does not exist")
67 | end
68 |
69 | begin
70 | connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'")
71 | rescue => e
72 | raise e, e.message.gsub(password, 'redacted'), e.backtrace
73 | end
74 |
75 | update_connection_quota_for_user
76 | create_read_only_user if read_only
77 | end
78 |
79 | def destroy
80 | connection.execute("DROP USER '#{username}'")
81 | ReadOnlyUser.find_by_username(username).try(:destroy)
82 | rescue ActiveRecord::StatementInvalid => e
83 | raise unless e.message =~ /DROP USER failed/
84 | end
85 |
86 | def to_json(*)
87 | obj = {
88 | 'credentials' => {
89 | 'hostname' => host,
90 | 'port' => port,
91 | 'name' => database_name,
92 | 'username' => username,
93 | 'password' => password,
94 | 'uri' => uri,
95 | 'jdbcUrl' => jdbc_url
96 | }
97 | }
98 |
99 | if Settings['tls_ca_certificate']
100 | obj['credentials']['ca_certificate'] = Settings['tls_ca_certificate']
101 | end
102 | obj.to_json
103 | end
104 |
105 | def self.update_all_max_user_connections
106 | # We would like to update these users in bulk by updating mysql.user
107 | # directly, but Galera does not replicate this table. DDL statments such
108 | # as GRANT USAGE must be used instead to ensure replication.
109 | Catalog.plans.each do |plan|
110 | users = connection.select_values(get_all_users_with_plan(plan))
111 | users.each do |user|
112 | connection.execute(update_max_user_connection_for_user(user, plan))
113 | end
114 | end
115 | end
116 |
117 | private
118 |
119 | def update_connection_quota_for_user
120 | max_user_connections = Catalog.connection_quota_for_plan_guid(service_instance.plan_guid)
121 |
122 | privileges = read_only ? "SELECT" : "ALL PRIVILEGES"
123 | grant_sql = "GRANT #{privileges} ON `#{service_instance.db_name}`.* TO '#{username}'@'%'"
124 | grant_sql = grant_sql + " WITH MAX_USER_CONNECTIONS #{max_user_connections}" if max_user_connections
125 | connection.execute(grant_sql)
126 |
127 | if !Settings.allow_table_locks
128 | revoke_sql = "REVOKE LOCK TABLES ON `#{service_instance.db_name}`.* FROM '#{username}'@'%'"
129 | connection.execute(revoke_sql)
130 | end
131 | end
132 |
133 | def create_read_only_user
134 | ReadOnlyUser.create(username: username, grantee: "'#{username}'@'%'")
135 | end
136 |
137 | def self.update_max_user_connection_for_user(user, plan)
138 | <<-SQL
139 | GRANT USAGE ON *.* TO '#{user}'@'%'
140 | WITH MAX_USER_CONNECTIONS #{plan.max_user_connections}
141 | SQL
142 | end
143 |
144 | def self.get_all_users_with_plan(plan)
145 | <<-SQL
146 | SELECT mysql.user.user
147 | FROM service_instances
148 | JOIN mysql.db ON service_instances.db_name=mysql.db.Db
149 | JOIN mysql.user ON mysql.user.User=mysql.db.User
150 | WHERE plan_guid='#{plan.id}' AND mysql.user.user NOT LIKE 'root'
151 | SQL
152 | end
153 |
154 | def connection_config
155 | Rails.configuration.database_configuration[Rails.env]
156 | end
157 |
158 | def uri
159 | "mysql://#{username}:#{password}@#{host}:#{port}/#{database_name}?reconnect=true"
160 | end
161 |
162 | def ssl_arguments
163 | return unless Settings['tls_ca_certificate']
164 | mysql_connector_j_flag = 'enabledTLSProtocols=TLSv1.2'
165 | mariadb_connector_j_flag = 'enabledSslProtocolSuites=TLSv1.2'
166 | "&useSSL=true{mysql_connector_j_flag}{mariadb_connector_j_flag}"
167 | end
168 |
169 | def jdbc_url
170 | "jdbc:mysql://#{host}:#{port}/#{database_name}?user=#{username}&password=#{password}#{ssl_arguments}"
171 | end
172 | end
173 |
--------------------------------------------------------------------------------
/app/models/service_instance.rb:
--------------------------------------------------------------------------------
1 | class ServiceInstance < ActiveRecord::Base
2 | def self.reserved_space_in_mb
3 | self.sum(:max_storage_mb)
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/service_instance_access_verifier.rb:
--------------------------------------------------------------------------------
1 | class ServiceInstanceAccessVerifier
2 | class << self
3 | def can_manage_instance?(instance_guid, cc_client)
4 | response_body = cc_client.get("/v2/service_instances/#{instance_guid}/permissions")
5 | !response_body.nil? && response_body['manage']
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/queries/service_instance_usage_query.rb:
--------------------------------------------------------------------------------
1 | class ServiceInstanceUsageQuery
2 | attr_reader :instance
3 |
4 | def initialize(instance)
5 | @instance = instance
6 | end
7 |
8 | def execute
9 | db_name = instance.db_name
10 | escaped_database = ActiveRecord::Base.sanitize(db_name)
11 | query = <<-SQL
12 | SELECT SUM(ROUND(((data_length + index_length) / 1024 / 1024), 2))
13 | FROM information_schema.TABLES
14 | WHERE table_schema = #{escaped_database}
15 | SQL
16 |
17 | result_set = ActiveRecord::Base.connection.execute(query).first
18 | result = result_set.first
19 | result.to_f
20 | end
21 |
22 | end
23 |
--------------------------------------------------------------------------------
/app/views/errors/approvals_error.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | This application requires the following permissions:
4 |
5 | - Access your profile data including your email address
6 | - View details of your applications and services
7 |
8 | <%= link_to "Manage third-party access", Configuration.manage_user_profile_url %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/views/errors/not_authorized.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Not Authorized: You do not have sufficient permissions for the space containing the requested service instance.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MySQL Management Dashboard
5 | <%= favicon_link_tag %>
6 | <%= stylesheet_link_tag "application", media: "all" %>
7 | <%= javascript_include_tag :application %>
8 | <%= csrf_meta_tags %>
9 |
10 |
11 |
12 |
13 |
27 |
28 |
29 |
35 |
36 | <%= yield %>
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/views/manage/instances/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= @used_data %> MB of <%= @quota %> MB used.
4 |
5 |
6 |
7 | <% if @over_quota %>
8 |
9 |
10 | Warning: Write permissions have been revoked due to storage utilization
11 | exceeding the plan quota. Read and delete permissions remain enabled.
12 | Write permissions will be restored when storage utilization has been
13 | reduced to below the plan quota.
14 |
15 |
16 | <% end %>
17 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | # Pick the frameworks you want:
4 | require 'active_record/railtie'
5 | require 'action_controller/railtie'
6 | # require 'action_mailer/railtie'
7 | # require 'sprockets/railtie'
8 | # require 'rails/test_unit/railtie'
9 |
10 | # Require the gems listed in Gemfile, including any gems
11 | # you've limited to :test, :development, or :production.
12 | Bundler.require(:default, Rails.env)
13 |
14 | require File.expand_path('../../lib/settings', __FILE__)
15 |
16 | module CfMysqlBroker
17 | class Application < Rails::Application
18 | # Settings in config/environments/* take precedence over those specified here.
19 | # Application configuration should go into files in config/initializers
20 | # -- all .rb files in that directory are automatically loaded.
21 |
22 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
23 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
24 | # config.time_zone = 'Central Time (US & Canada)'
25 |
26 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
27 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
28 | # config.i18n.default_locale = :de
29 |
30 | config.api_only = false
31 |
32 | config.assets.enabled = true
33 |
34 | config.autoload_paths += %W(#{config.root}/lib)
35 | config.autoload_paths += Dir["#{config.root}/lib/**/"]
36 |
37 | config.paths.add 'config/database', with: Settings.database_config_path
38 | config.middleware.insert_after 'Rack::Runtime', Rack::MethodOverride
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
5 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # This file should not be used in deployed environments. Instead, the
2 | # application configuration file specified by SETTINGS_PATH should
3 | # include the path to a database configuration that includes these
4 | # settings.
5 |
6 |
7 | # This is included because our app connects to the database when Rails starts,
8 | # and Rails gets loaded when static assets are being precompiled in the 'assets'
9 | # environment.
10 | assets:
11 | adapter: mysql2
12 | encoding: utf8
13 | database: development
14 | pool: 5
15 | username: root
16 | password:
17 | host: localhost
18 | port: 3306
19 |
20 | development:
21 | adapter: mysql2
22 | encoding: utf8
23 | database: development
24 | pool: 5
25 | username: root
26 | password:
27 | host: localhost
28 | port: 3306
29 |
30 | # Warning: The database defined as "test" will be erased and
31 | # re-generated from your development database when you run "rake".
32 | # Do not set this db to the same as development or production.
33 | test:
34 | adapter: mysql2
35 | encoding: utf8
36 | database: test
37 | pool: 5
38 | username: root
39 | password:
40 | host: localhost
41 | port: 3306
42 |
43 | ci_test:
44 | adapter: mysql2
45 | encoding: utf8
46 | database: test
47 | pool: 5
48 | username: root
49 | password:
50 | host: localhost
51 | port: 3306
52 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | CfMysqlBroker::Application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/assets.rb:
--------------------------------------------------------------------------------
1 | CfMysqlBroker::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both thread web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20 | # config.action_dispatch.rack_cache = true
21 |
22 | config.serve_static_files = true
23 | config.static_cache_control = 'public, max-age=3600'
24 | config.assets.compile = false
25 | config.assets.digest = true
26 |
27 | # Specifies the header that your server uses for sending files.
28 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for apache
29 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
30 |
31 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
32 | # config.force_ssl = true
33 |
34 | # Set to :debug to see everything in the log.
35 | config.log_level = :info
36 |
37 | # Prepend all log lines with the following tags.
38 | # config.log_tags = [ :subdomain, :uuid ]
39 |
40 | # Use a different logger for distributed setups.
41 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
42 |
43 | # Use a different cache store in production.
44 | # config.cache_store = :mem_cache_store
45 |
46 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
47 | # config.action_controller.asset_host = 'http://assets.example.com'
48 |
49 | # Ignore bad email addresses and do not raise email delivery errors.
50 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
51 | # config.action_mailer.raise_delivery_errors = false
52 |
53 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
54 | # the I18n.default_locale when a translation can not be found).
55 | config.i18n.fallbacks = true
56 |
57 | # Send deprecation notices to registered listeners.
58 | config.active_support.deprecation = :notify
59 |
60 | # Disable automatic flushing of the log to improve performance.
61 | # config.autoflush_log = false
62 |
63 | # Use default logging formatter so that PID and timestamp are not suppressed.
64 | config.log_formatter = ::Logger::Formatter.new
65 | end
66 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | CfMysqlBroker::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | # config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 | end
22 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | CfMysqlBroker::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both thread web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20 | # config.action_dispatch.rack_cache = true
21 |
22 | config.serve_static_files = true
23 | config.static_cache_control = 'public, max-age=3600'
24 | config.assets.compile = false
25 | config.assets.digest = true
26 |
27 | # Specifies the header that your server uses for sending files.
28 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for apache
29 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
30 |
31 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
32 | # config.force_ssl = true
33 |
34 | # Set to :debug to see everything in the log.
35 | config.log_level = :info
36 |
37 | # Prepend all log lines with the following tags.
38 | # config.log_tags = [ :subdomain, :uuid ]
39 |
40 | # Use a different logger for distributed setups.
41 | config.logger = Logger.new(STDOUT)
42 |
43 | # Use a different cache store in production.
44 | # config.cache_store = :mem_cache_store
45 |
46 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
47 | # config.action_controller.asset_host = 'http://assets.example.com'
48 |
49 | # Ignore bad email addresses and do not raise email delivery errors.
50 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
51 | # config.action_mailer.raise_delivery_errors = false
52 |
53 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
54 | # the I18n.default_locale when a translation can not be found).
55 | config.i18n.fallbacks = true
56 |
57 | # Send deprecation notices to registered listeners.
58 | config.active_support.deprecation = :notify
59 |
60 | # Disable automatic flushing of the log to improve performance.
61 | # config.autoflush_log = false
62 |
63 | # Use default logging formatter so that PID and timestamp are not suppressed.
64 | config.log_formatter = ::Logger::Formatter.new
65 | end
66 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | CfMysqlBroker::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static asset server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | # config.action_mailer.delivery_method = :test
33 |
34 | # Print deprecation notices to the stderr.
35 | config.active_support.deprecation = :stderr
36 | end
37 |
--------------------------------------------------------------------------------
/config/environments/travis_test.rb:
--------------------------------------------------------------------------------
1 | CfMysqlBroker::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static asset server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | # config.action_mailer.delivery_method = :test
33 |
34 | # Print deprecation notices to the stderr.
35 | config.active_support.deprecation = :stderr
36 | end
37 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | Rails.application.config.action_dispatch.cookies_serializer = :json
2 |
--------------------------------------------------------------------------------
/config/initializers/omniauth.rb:
--------------------------------------------------------------------------------
1 | unless Rails.env.assets?
2 | OmniAuth.config.failure_raise_out_environments = []
3 | OmniAuth.config.path_prefix = '/manage/auth'
4 |
5 | client = Settings.services[0].dashboard_client
6 |
7 | Rails.application.config.middleware.use OmniAuth::Builder do
8 | unless (Rails.env.ci_test? || Rails.env.test? || Rails.env.development?)
9 | provider :cloudfoundry, client.id, client.secret, {
10 | auth_server_url: Configuration.auth_server_url,
11 | token_server_url: Configuration.token_server_url,
12 | scope: %w(cloud_controller_service_permissions.read openid),
13 | skip_ssl_validation: Settings.skip_ssl_validation
14 | }
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | Rails.application.config.session_store :cookie_store, key: '_cf_mysql_broker_session'
2 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper
4 |
5 | # Enable parameter wrapping for JSON.
6 | # ActiveSupport.on_load(:action_controller) do
7 | # wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
8 | # end
9 |
10 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | CfMysqlBroker::Application.routes.draw do
2 | resource :preview, only: [:show]
3 |
4 | namespace :v2 do
5 | resource :catalog, only: [:show]
6 | patch 'service_instances/:id' => 'service_instances#set_plan'
7 | resources :service_instances, only: [:update, :destroy] do
8 | resources :service_bindings, only: [:update, :destroy]
9 | end
10 | end
11 |
12 | namespace :manage do
13 | get 'auth/cloudfoundry/callback' => 'auth#create'
14 | get 'auth/failure' => 'auth#failure'
15 | delete 'auth' => 'auth#destroy'
16 | resources :instances, only: [:show]
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | development:
2 | secret_token: <%= Settings.cookie_secret %>
3 |
4 | test:
5 | secret_token: <%= Settings.cookie_secret %>
6 |
7 | production:
8 | secret_token: <`%= Settings.cookie_secret %>
9 |
--------------------------------------------------------------------------------
/config/settings.yml:
--------------------------------------------------------------------------------
1 | # This file should not be used in deployed environments. Instead, set
2 | # the SETTINGS_PATH environment variable to point to a configuration
3 | # file that contains these settings.
4 |
5 | defaults: &defaults
6 | allow_table_locks: true
7 | database_config_path: 'config/database.yml'
8 | auth_username: cc
9 | cookie_secret: e7247dae-a252-4393-afa3-2219c1c02efd
10 | session_expiry: 86400
11 | message_bus_servers:
12 | - nats://127.0.0.1:4222
13 | external_host: p-mysql.bosh-lite.com
14 | external_ip: 127.0.0.1
15 | ssl_enabled: false
16 | skip_ssl_validation: true
17 | persistent_disk: 10000
18 | gcache_size: 50
19 | ib_log_file_size: 100
20 |
21 | services:
22 | - name: p-mysql-local
23 | id: 3101b971-1044-4816-a7ac-9ded2e028079
24 | description: MySQL service for application development and testing
25 | plan_updateable: true
26 | tags:
27 | - mysql
28 | - relational
29 | metadata:
30 | displayName: "Pivotal MySQL Dev"
31 | imageUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALIAAABMCAYAAADJEu0BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjBDOTlBMkE3NDQ3MDExRTNCN0I5RDc0MjE2NTBCMjgzIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjBDOTlBMkE4NDQ3MDExRTNCN0I5RDc0MjE2NTBCMjgzIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MEM5OUEyQTU0NDcwMTFFM0I3QjlENzQyMTY1MEIyODMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MEM5OUEyQTY0NDcwMTFFM0I3QjlENzQyMTY1MEIyODMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7RmE0vAAAGn0lEQVR42uycTYgjRRTH38heFNTJiFdlO3rx2nMQQXbADAwePAg94ElByJxW0Eui4HmSo3OQ3RZkERFMHwTxIpODtznMNLuuKIim/dZZD7b4wXrZjZ3wauZtWZ2uTqfjJP3/QTGTTnem+tW/Xr33uiYr1L44pBIZ7u6tEAAlcxdMACBkACBkACBkACBksHycyzrh9ae2aO3ue05exzdv0sEP39L+118S3b4NC4LFEPKL64/Tw6tr/zn+6fHPtPH2m/T7n3/AiuDsC1mxdeUS3fj7L3ro/lV6+YkLtOE8Sm9sPUPPB+/CimBxYuTrvx7TtR+/pw8/v07Pvv/O+NiT5+uwIECyB8DcQ4sRq/feR4898CC9dqExfv1J9BUsCBZPyB889wJtnH9k/PthEma88vFHsCBYPCG/dXhAvc+u0rVffqKD774hGg5hQbB4Qn7v6iEsBpDsAQAhA1AktPjixvH4sfQ/t27BWmBxhfz0lUuwEkBoAQCEDACEDCBkACBkAObPyhCPmQE8MgAQMgAQMgAQMli+ZI/aF0c/vaS5SYuT1s1xvbpuRLtgX2pJayZt9O8njjgeJK3PbRINbp523OfPiDKub3Efit6H+hydmPsQzHIAh7t7ULEQck8IYNNCNIqBEF2Rb910uQ8OD3gsxK1EEXLfYsMEuCz6H2nv1YSg24br9Xsp+u2hA20imgTd575EEPJsQwtXHGtaXtsQwivqifeFNxx9iUad2xqLN+D3Y8ME2GcRB3xuXbt+mydBU/ydebBtaF2+h1F/j3LYGmQgd79FbOSG5bVqEPqG5TwPTSFiU1ijwopaiid2J1yrQpOAz1ViXp+DbYOUY23uR4f7VMsZzgGLZE95vpalR+7PYHlsTBh4fUnWY1GXRWAjhB3+G67l/ZWJL8KklsUq4RR0FpUUMll4ZWV8Uyzds4gTG3xOS4uH8yaGcc7krJszfCqTkPtTY+9sosN2GrBdh/zzxFYrr77US9ogaan2Tt5r8DmtqghZZdUNLW42CVGvcDhigLI8iMfnRHw+iWXWtlpSm6ICEPLkc3KEUGUiY2Z9oh6Jia5WnUDE17Up7V0JIZPwst6ECkPDICJXLJtxhtfzxKTp8mC4YvBslloSkyCvmCljos6TmO9X9kfG/uv8s80JY5vvv5XD3qPxioa7e0GVhOyzyLwpY9o4w+up5M4Xxzb5tcPL6W+clKWJ2k2Jm22Ipghl5pEUOuLePDHJ08Kj8fgk4jyxdxI6eIawosmf7VchRnYMxk1brppiiZ6UzKTFop5hIsScjNXZ44Q8CToiljZ55Gk94FnGs0h+I20i+hNWUdtkeuk8srxpz2AUx8IofSHGmiZAFZZEKQPU5eW0LgZIlaqoQEgxi0lQBk6KkGWip7c7auqJVz6xd+KBZSKonFGQnBNVUcghN08zdDPH7A5EdUEfJJsnhxF76XX+vSm8S1hAlM4MJkMZQg4ybCmbbwgVitp7KYWc5pXz1I59Q0aukrw88Zo8XxfyNLVVVXE5C0uty80kNPmEsm1oXYvx8jjJ86ssZF2IrZwlL5WEuFqbRkBRSuji5hRziz3gWfFQLYPHDLVJawWHDuMHPklIMW4F7L1UQtaFqDyZnyP2lElInrAk7bNjQ/besaxAqCd6eR+ilIXa6BRq3jWckCjbVkCalkljJYRMWrLVSDHKpCdzfRFrN8Rr3Su1LAXoa4PWZZHvU/YDAbkx6f9MfEYiOxLVn03t/a6wWZpdaiZ7c504otPtrP3kWFgFIZ/LKM/0hWHSzoksvERH86K6yFweWCX0WCyvnhCgXjpTx0af36PTDUaR8ORyr/WORXw+sKjI7FjYdpCxak3aVrrDE6/DfQ/ptOSmwql2yr34GfZeWiFnLcvKMFm14ywhpyV5m+x5vJTlNOSBTfMs6tHtZTrdXG8S3zbZ1ZCzKiG2satjWLkiUXWY5ABCrtZ02C5eSlVpkr3jKiR5CrWxvuzM/IgHb8di8F1twPKEATVNaMrT10Qsumgxo35Pfbqzhkwg5zfWF4gJiexKblHB+FUvq6k4usX96C2goGOqSMJWZrI3C7yMpXAeQlCbb9R+5B6vEgBCtiJv7blMIo6T62T3z6igwqGFJ+JaVT6Kzlj2rAQNIOSJ8XBDW9YhGrBwQvbptJjfZ0+MJRyUDr6NEyDZAwBCBgBCBgBCBhAyABAyABAyABAygJABgJABgJABgJABhAwAhAwAhAwAhAwgZAAgZAAgZAAgZAAgZLBc/CvAALNW7XVv+EUjAAAAAElFTkSuQmCC"
32 | longDescription: "A MySQL relational database service for development and testing. The MySQL server is multi-tenant and is not replicated."
33 | providerDisplayName: "Pivotal Software"
34 | documentationUrl: "http://docs.run.pivotal.io"
35 | supportUrl: "http://support.run.pivotal.io/home"
36 | dashboard_client:
37 | id: 'p-mysql'
38 | secret: 'p-mysql'
39 | redirect_uri: 'http://10.80.130.98:3000/manage/auth/cloudfoundry/callback'
40 | plans:
41 | - name: 5mb
42 | id: 2451fa22-df16-4c10-ba6e-1f682d3dcdc9
43 | description: Free Trial
44 | max_storage_mb: 5
45 | max_user_connections: 5
46 | displayName: 5MB
47 | metadata:
48 | costs:
49 | - amount:
50 | usd: 0.0
51 | unit: MONTHLY
52 |
53 | bullets:
54 | - Shared MySQL server
55 | - 5 MB storage
56 | - 40 concurrent connections
57 | - name: 10mb
58 | id: f488f238-f364-4712-808d-cacfc49db053
59 | description: Free Trial - 10MB
60 | max_storage_mb: 10
61 | max_user_connections: 10
62 | displayName: 10MB
63 | metadata:
64 | costs:
65 | - amount:
66 | usd: 0.0
67 | unit: MONTHLY
68 |
69 | bullets:
70 | - Shared MySQL server
71 | - 10 MB storage
72 | - 40 concurrent connections
73 |
74 | cc_api_uri: 'https://api.bosh-lite.com'
75 |
76 | assets:
77 | auth_password: 'secret'
78 | <<: *defaults
79 |
80 | development:
81 | auth_password: 'secret'
82 | <<: *defaults
83 |
84 | test:
85 | auth_password: 'secret'
86 | <<: *defaults
87 |
88 | ci_test:
89 | auth_password: 'secret'
90 | <<: *defaults
91 |
--------------------------------------------------------------------------------
/db/migrate/20140529214059_create_service_instances.rb:
--------------------------------------------------------------------------------
1 | class CreateServiceInstances < ActiveRecord::Migration
2 | class ServiceInstance < ActiveRecord::Base
3 | end
4 |
5 | class DatabaseNameToServiceInstanceGuidConverter
6 | DATABASE_PREFIX = 'cf_'.freeze
7 |
8 | def self.guid_from_database_name(database_name)
9 | guid = database_name.sub(DATABASE_PREFIX, '').gsub('_', '-')
10 |
11 | # MySQL database names are limited to [0-9,a-z,A-Z$_] and 64 chars
12 | if guid =~ /[^0-9,a-z,A-Z$-]+/
13 | raise 'Only ids matching [0-9,a-z,A-Z$-]+ are allowed'
14 | end
15 |
16 | guid
17 | end
18 | end
19 |
20 | def up
21 | services = Settings['services']
22 | plans = services.first['plans']
23 | plan_guid = plans.first['id']
24 |
25 | raise "Migration cannot be performed: no service plans found" unless (1 == services.length && plans.length >= 1 )
26 | puts "Migration will associate existing service instances with the first service plan from the catalog (id: #{plan_guid})" unless (1 == services.length && 1 == plans.length)
27 |
28 | create_table :service_instances do |t|
29 | t.string :guid
30 | t.string :plan_guid
31 | end
32 |
33 | schema_names = connection.select_values("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE schema_name LIKE 'cf\\_%'")
34 | schema_names.each do |name|
35 | guid = DatabaseNameToServiceInstanceGuidConverter.guid_from_database_name(name)
36 | ServiceInstance.create(guid: guid, plan_guid: plan_guid)
37 | end
38 | end
39 |
40 | def down
41 | services = Settings['services']
42 | plans = services.first['plans']
43 | raise 'Migration can only be run if the catalog has a single service with one plan' unless (1 == services.length && 1 == plans.length)
44 |
45 | drop_table :service_instances
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/db/migrate/20140616163621_add_quota_to_service_instances.rb:
--------------------------------------------------------------------------------
1 | class AddQuotaToServiceInstances < ActiveRecord::Migration
2 | def change
3 | add_column :service_instances, :max_storage_mb, :integer, null: false, default: 0
4 | add_index :service_instances, :guid
5 | add_index :service_instances, :plan_guid
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20140616212709_add_db_name_to_service_instances.rb:
--------------------------------------------------------------------------------
1 | class AddDbNameToServiceInstances < ActiveRecord::Migration
2 | def change
3 | add_column :service_instances, :db_name, :string
4 | add_index :service_instances, :db_name
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20140918004518_set_db_name.rb:
--------------------------------------------------------------------------------
1 | class SetDbName < ActiveRecord::Migration
2 | def up
3 | ServiceInstance.reset_column_information
4 | ServiceInstance.find_each do |instance|
5 | instance.db_name = "cf_#{instance.guid.gsub('-', '_')}"
6 | instance.save!
7 | end
8 | end
9 |
10 | def down
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20180117002358_create_read_only_users.rb:
--------------------------------------------------------------------------------
1 | class CreateReadOnlyUsers < ActiveRecord::Migration
2 | def change
3 | create_table :read_only_users do |t|
4 | t.string :username
5 | t.string :grantee
6 |
7 | t.timestamps null: false
8 | end
9 |
10 | add_index :read_only_users, :username
11 | add_index :read_only_users, :grantee
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20180117002358) do
15 |
16 | create_table "read_only_users", force: :cascade do |t|
17 | t.string "username", limit: 255
18 | t.string "grantee", limit: 255
19 | t.datetime "created_at", null: false
20 | t.datetime "updated_at", null: false
21 | end
22 |
23 | add_index "read_only_users", ["grantee"], name: "index_read_only_users_on_grantee", using: :btree
24 | add_index "read_only_users", ["username"], name: "index_read_only_users_on_username", using: :btree
25 |
26 | create_table "service_instances", force: :cascade do |t|
27 | t.string "guid", limit: 255
28 | t.string "plan_guid", limit: 255
29 | t.integer "max_storage_mb", limit: 4, default: 0, null: false
30 | t.string "db_name", limit: 255
31 | end
32 |
33 | add_index "service_instances", ["db_name"], name: "index_service_instances_on_db_name", using: :btree
34 | add_index "service_instances", ["guid"], name: "index_service_instances_on_guid", using: :btree
35 | add_index "service_instances", ["plan_guid"], name: "index_service_instances_on_plan_guid", using: :btree
36 |
37 | end
38 |
--------------------------------------------------------------------------------
/lib/configuration.rb:
--------------------------------------------------------------------------------
1 | module Configuration
2 | extend self
3 |
4 | def documentation_url
5 | Settings.services.first.metadata.documentationUrl rescue nil
6 | end
7 |
8 | def support_url
9 | Settings.services.first.metadata.supportUrl rescue nil
10 | end
11 |
12 | def manage_user_profile_url
13 | "#{auth_server_url}/profile"
14 | end
15 |
16 | def auth_server_url
17 | cc_api_info["authorization_endpoint"]
18 | end
19 |
20 | def auth_server_logout_url
21 | "#{cc_api_info["authorization_endpoint"]}/logout.do"
22 | end
23 |
24 | def token_server_url
25 | cc_api_info["token_endpoint"]
26 | end
27 |
28 | def cc_api_info
29 | return store[:cc_api_info] unless store[:cc_api_info].nil?
30 |
31 | cc_client = CloudControllerHttpClient.new
32 | response = cc_client.get('/info')
33 |
34 | store[:cc_api_info] = response
35 | end
36 |
37 | def store
38 | @store ||= {}
39 | end
40 |
41 | def clear
42 | store.clear
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/request_response_logger.rb:
--------------------------------------------------------------------------------
1 | class RequestResponseLogger
2 | attr_reader :logger
3 | attr_reader :message_type
4 |
5 | def initialize(message_type, logger)
6 | @message_type = message_type
7 | @logger = logger
8 | end
9 |
10 | def log_headers_and_body(headers, body, log_all_headers=false)
11 | headers_to_log = log_all_headers ? headers : remove_non_permitted_headers(headers)
12 |
13 | request_summary = {
14 | headers: filtered_headers(headers_to_log),
15 | body: body
16 | }
17 |
18 | logger.info " #{message_type} #{request_summary.to_json}"
19 | end
20 |
21 | private
22 |
23 | def filtered_headers(headers)
24 | headers.keys.each do |k|
25 | headers[k] = '[PRIVATE DATA HIDDEN]' if filtered_keys.include?(k)
26 | end
27 |
28 | headers
29 | end
30 |
31 | def remove_non_permitted_headers(headers)
32 | headers.select { |key, _| permitted_keys.include? key }
33 | end
34 |
35 | def permitted_keys
36 | %w(CONTENT_LENGTH
37 | CONTENT_TYPE
38 | GATEWAY_INTERFACE
39 | PATH_INFO
40 | QUERY_STRING
41 | REMOTE_ADDR
42 | REMOTE_HOST
43 | REQUEST_METHOD
44 | REQUEST_URI
45 | SCRIPT_NAME
46 | SERVER_NAME
47 | SERVER_PORT
48 | SERVER_PROTOCOL
49 | SERVER_SOFTWARE
50 | HTTP_ACCEPT
51 | HTTP_USER_AGENT
52 | HTTP_AUTHORIZATION
53 | HTTP_X_VCAP_REQUEST_ID
54 | HTTP_X_BROKER_API_VERSION
55 | HTTP_HOST
56 | HTTP_VERSION
57 | REQUEST_PATH)
58 | end
59 |
60 | def filtered_keys
61 | %w(HTTP_AUTHORIZATION)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/service_capacity.rb:
--------------------------------------------------------------------------------
1 | class ServiceCapacity
2 | def self.can_allocate?(new_db_storage_mb)
3 | estimated_overhead + ServiceInstance.reserved_space_in_mb + new_db_storage_mb <= Settings['persistent_disk']
4 | end
5 |
6 | private
7 |
8 | def self.estimated_overhead
9 | #constant overhead on initialize of cluster values are in MB
10 | ibdata_file_size = 12
11 | broker_db_size = 0.26
12 | gcache_size = Settings['gcache_size']
13 | mysql_db_size = 1.2
14 | performance_db_size = 0.22
15 |
16 | #despite innodb_log_per_table=true, there are only ever 2 ib_log files
17 | ib_log_file_size = 2 * Settings['ib_log_file_size']
18 |
19 | current_db_overhead = 0.073 * ServiceInstance.all.count
20 |
21 | #can edit, for the sake of safety... undeleted GRA_*.logs & other logs
22 | buffer = 50
23 |
24 | ibdata_file_size + broker_db_size + gcache_size +
25 | mysql_db_size + performance_db_size + ib_log_file_size +
26 | current_db_overhead + buffer
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/service_instance_manager.rb:
--------------------------------------------------------------------------------
1 | class ServiceInstanceManager
2 | class ServiceInstanceNotFound < StandardError; end
3 | class ServicePlanNotFound < StandardError; end
4 | class InvalidServicePlanUpdate < StandardError; end
5 |
6 | DATABASE_PREFIX = 'cf_'.freeze
7 |
8 | def self.create(opts)
9 | guid = opts[:guid]
10 | plan_guid = opts[:plan_guid]
11 |
12 | unless Catalog.has_plan?(plan_guid)
13 | raise "Plan #{plan_guid} was not found in the catalog."
14 | end
15 |
16 | max_storage_mb = Catalog.storage_quota_for_plan_guid(plan_guid)
17 |
18 | if guid =~ /[^0-9,a-z,A-Z$-]+/
19 | raise 'Only GUIDs matching [0-9,a-z,A-Z$-]+ are allowed'
20 | end
21 |
22 | db_name = database_name_from_service_instance_guid(guid)
23 |
24 | Database.create(db_name)
25 | ServiceInstance.create(guid: guid, plan_guid: plan_guid, max_storage_mb: max_storage_mb, db_name: db_name)
26 | end
27 |
28 | def self.set_plan(opts)
29 | guid = opts[:guid]
30 | plan_guid = opts[:plan_guid]
31 |
32 | unless Catalog.has_plan?(plan_guid)
33 | raise ServicePlanNotFound.new("Plan #{plan_guid} was not found in the catalog.")
34 | end
35 |
36 | instance = ServiceInstance.find_by_guid(guid)
37 | raise ServiceInstanceNotFound if instance.nil?
38 |
39 | if Database.usage(database_name_from_service_instance_guid(guid)) > Catalog.storage_quota_for_plan_guid(plan_guid)
40 | raise InvalidServicePlanUpdate.new('Downgrading this service instance will violate the quota of the new plan')
41 | end
42 |
43 | instance.plan_guid = plan_guid
44 | instance.max_storage_mb = Catalog.storage_quota_for_plan_guid(plan_guid)
45 | instance.save
46 | end
47 |
48 | def self.destroy(opts)
49 | guid = opts[:guid]
50 | instance = ServiceInstance.find_by_guid(guid)
51 | raise ServiceInstanceNotFound if instance.nil?
52 | instance.destroy
53 | Database.drop(database_name_from_service_instance_guid(guid))
54 | end
55 |
56 | def self.database_name_from_service_instance_guid(guid)
57 | "#{DATABASE_PREFIX}#{guid.gsub('-', '_')}"
58 | end
59 |
60 | def self.sync_service_instances
61 | Catalog.plans.each do |plan|
62 | service_instances = ServiceInstance.where(plan_guid: plan.id)
63 | service_instances.update_all(max_storage_mb: plan.max_storage_mb)
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/settings.rb:
--------------------------------------------------------------------------------
1 | ENV['SETTINGS_PATH'] ||= File.expand_path('../../config/settings.yml', __FILE__)
2 |
3 | class Settings < Settingslogic
4 | source ENV['SETTINGS_PATH']
5 | namespace Rails.env
6 | end
7 |
--------------------------------------------------------------------------------
/lib/table_lock_manager.rb:
--------------------------------------------------------------------------------
1 | class TableLockManager
2 | def self.update_table_lock_permissions
3 | connection = ActiveRecord::Base.connection
4 | if Settings.allow_table_locks
5 | connection.select_rows("SELECT User, Db, Host FROM mysql.db WHERE Lock_tables_priv='N'").each do |user, db, host|
6 | next unless user.present?
7 | connection.execute "GRANT LOCK TABLES ON `#{db}`.* TO '#{user}'@'#{host}'"
8 | end
9 | else
10 | connection.select_rows("SELECT User, Db, Host FROM mysql.db WHERE Lock_tables_priv='Y'").each do |user, db, host|
11 | next unless user.present? && db.present?
12 | connection.execute "REVOKE LOCK TABLES ON `#{db}`.* FROM '#{user}'@'#{host}'"
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/tasks/brakeman.rake:
--------------------------------------------------------------------------------
1 | namespace :brakeman do
2 |
3 | desc "Run Brakeman"
4 | task :run, :output_files do |t, args|
5 | require 'brakeman'
6 |
7 | files = args[:output_files].split(' ') if args[:output_files]
8 | Brakeman.run :app_path => ".", :output_files => files, :print_report => true
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/tasks/broker.rake:
--------------------------------------------------------------------------------
1 | namespace :broker do
2 |
3 | desc "Update properties of existing service instances to match plans in Settings"
4 | task :sync_plans_in_db do
5 | require File.expand_path('../../../config/environment', __FILE__)
6 | ServiceInstanceManager.sync_service_instances
7 | ServiceBinding.update_all_max_user_connections
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/tasks/table_locks.rake:
--------------------------------------------------------------------------------
1 | namespace :table_locks do
2 |
3 | desc "Update Table Lock Permissions"
4 | task :update_table_lock_permissions => :environment do
5 | TableLockManager.update_table_lock_permissions
6 | end
7 |
8 | end
9 |
--------------------------------------------------------------------------------
/lib/uaa_session.rb:
--------------------------------------------------------------------------------
1 | class UaaSession
2 | class << self
3 | def build(access_token, refresh_token)
4 | token_info = existing_token_info(access_token, refresh_token)
5 | if token_expired?(token_info)
6 | token_info = refreshed_token_info(refresh_token)
7 | end
8 |
9 | new(token_info)
10 | end
11 |
12 | private
13 |
14 | def token_expired?(token_info)
15 | header = token_info.auth_header
16 | expiry = CF::UAA::TokenCoder.decode(header.split()[1], verify: false)['exp']
17 | expiry.is_a?(Integer) && expiry <= Time.now.to_i
18 | end
19 |
20 | def existing_token_info(access_token, refresh_token)
21 | CF::UAA::TokenInfo.new(access_token: access_token,
22 | refresh_token: refresh_token,
23 | token_type: 'bearer')
24 | end
25 |
26 | def refreshed_token_info(refresh_token)
27 | dashboard_client = Settings.services[0].dashboard_client
28 | client = CF::UAA::TokenIssuer.new(
29 | Configuration.auth_server_url,
30 | dashboard_client.id,
31 | dashboard_client.secret,
32 | { token_target: Configuration.token_server_url }
33 | )
34 | client.refresh_token_grant(refresh_token)
35 | end
36 | end
37 |
38 | def initialize(token_info)
39 | @token_info = token_info
40 | end
41 |
42 | def auth_header
43 | token_info.auth_header
44 | end
45 |
46 | def access_token
47 | token_info.info[:access_token] || token_info.info["access_token"]
48 | end
49 |
50 | def refresh_token
51 | token_info.info[:refresh_token] || token_info.info["refresh_token"]
52 | end
53 |
54 | private
55 |
56 | attr_reader :token_info
57 | end
58 |
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/log/.keep
--------------------------------------------------------------------------------
/manifest.yml:
--------------------------------------------------------------------------------
1 | ---
2 | applications:
3 | - name: cfmysql
4 | memory: 256M
5 | instances: 1
6 | host: cfmysql
7 | path: .
8 | env:
9 | AUTH_TOKEN: secret
10 |
--------------------------------------------------------------------------------
/spec/controllers/manage/auth_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Manage::AuthController do
4 |
5 | describe '#create' do
6 | let(:instance_id) { 'abc-123' }
7 |
8 | before do
9 | session[:instance_id] = instance_id
10 | request.env['omniauth.auth'] = {
11 | 'extra' => extra,
12 | 'credentials' => credentials
13 | }
14 | end
15 |
16 | context 'when access token, refresh token, and user_id are present' do
17 | let(:extra) {
18 | {
19 | 'raw_info' => {
20 | 'user_id' => 'mister_tee'
21 | }
22 | }
23 | }
24 |
25 | let(:credentials) {
26 | {
27 | 'token' => 'UAA access token',
28 | 'refresh_token' => 'UAA refresh token'
29 | }
30 | }
31 |
32 | it 'authenticates the user based on the permissions from UAA' do
33 | get :create, some: 'stuff'
34 | expect(response.status).to eql(302)
35 | expect(response).to redirect_to(manage_instance_path(instance_id))
36 |
37 | expect(session[:uaa_user_id]).to eql('mister_tee')
38 | expect(session[:uaa_access_token]).to eql('UAA access token')
39 | expect(session[:uaa_refresh_token]).to eql('UAA refresh token')
40 | expect(session[:last_seen]).to be_a_kind_of(Time)
41 | end
42 | end
43 |
44 | context 'when omniauth does not yield an access token' do
45 | let(:extra) {
46 | {
47 | 'raw_info' => {
48 | 'user_id' => 'mister_tee'
49 | }
50 | }
51 | }
52 |
53 | let(:credentials) {
54 | {
55 | 'token' => '',
56 | 'refresh_token' => '',
57 | 'authorized_scopes' => ''
58 | }
59 | }
60 |
61 | it 'renders the approvals error page' do
62 | get :create, some: 'stuff'
63 |
64 | expect(response.status).to eql(200)
65 | expect(response).to render_template 'errors/approvals_error'
66 | end
67 | end
68 |
69 | context 'when omniauth does not yield user info (raw_info)' do
70 | let(:extra) {
71 | {}
72 | }
73 |
74 | let(:credentials) {
75 | {
76 | 'token' => 'access token',
77 | 'refresh_token' => 'refresh token',
78 | 'authorized_scopes' => 'scope.yay'
79 | }
80 | }
81 |
82 | it 'renders the approvals error page' do
83 | get :create, some: 'stuff'
84 |
85 | expect(response.status).to eql(200)
86 | expect(response).to render_template 'errors/approvals_error'
87 | end
88 | end
89 | end
90 |
91 | describe '#failure' do
92 | it 'returns a 403 status code' do
93 | get :failure
94 | expect(response.status).to eql(403)
95 | end
96 |
97 | it 'echos the passed message param as the failure message' do
98 | get :failure, message: 'something broke'
99 | expect(response.body).to eq 'something broke'
100 | end
101 | end
102 |
103 | describe '#destroy' do
104 | before do
105 | session[:foo] = 'bar'
106 | allow(Configuration).to receive(:auth_server_logout_url).and_return('auth_server_logout_url')
107 | end
108 |
109 | it 'destroys the session' do
110 | delete :destroy
111 | expect(session).to be_empty
112 | end
113 |
114 | it 'redirects to GET #{auth_server}/logout.do' do
115 | delete :destroy
116 | expect(response).to redirect_to 'auth_server_logout_url'
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/spec/controllers/manage/instances_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Manage::InstancesController do
4 |
5 | describe 'show' do
6 | render_views
7 |
8 | before do
9 | allow(Settings).to receive(:ssl_enabled).and_return(false)
10 | allow(Settings).to receive(:cc_api_uri) { 'http://api.example.com' }
11 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('scope' => ['openid', 'cloud_controller_service_permissions.read'])
12 | end
13 |
14 | describe 'redirecting a user that is not logged in' do
15 | context 'when there is no session' do
16 | it 'redirects the user' do
17 | get :show, id: 'abc-123'
18 | expect(response).to redirect_to('/manage/auth/cloudfoundry')
19 | end
20 | end
21 |
22 | context 'when there is a session with a uaa_user_id' do
23 | let(:expiry) { 5 } #seconds
24 | before do
25 | allow(Settings).to receive(:session_expiry).and_return(expiry)
26 |
27 | session[:uaa_user_id] = 'some-user-id'
28 | session[:uaa_access_token] = ''
29 | session[:uaa_refresh_token] = ''
30 | end
31 |
32 | context 'when the last_seen is old' do
33 | before do
34 | session[:last_seen] = Time.now - (expiry+1)
35 | end
36 |
37 | it 'redirects to auth' do
38 | get :show, id: 'abc-123'
39 | expect(response).to redirect_to('/manage/auth/cloudfoundry')
40 | end
41 | end
42 | end
43 | end
44 |
45 | describe 'the dashboard redirects depending on ssl_enabled setting' do
46 |
47 | let(:instance) { ServiceInstance.new(id: 'abc-123') }
48 | let(:uaa_session) { double(UaaSession, auth_header: 'bearer ', refresh_token: 'new_refresh_token') }
49 |
50 | before do
51 | instance.save
52 |
53 | session[:uaa_user_id] = 'some-user-id'
54 | session[:uaa_access_token] = ''
55 | session[:uaa_refresh_token] = ''
56 | session[:last_seen] = Time.now
57 |
58 | allow(UaaSession).to receive(:build).with('', '').and_return(uaa_session)
59 |
60 | allow(uaa_session).to receive(:access_token).and_return('new_access_token')
61 |
62 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?)
63 | end
64 |
65 | after { instance.destroy }
66 |
67 | context 'when ssl_enabled is false' do
68 | before do
69 | allow(Settings).to receive(:ssl_enabled).and_return(false)
70 | end
71 |
72 | it 'does not redirect http requests to https' do
73 | @request.env['HTTPS'] = nil
74 | get :show, id: 'abc-123'
75 | expect(response.status).to eq 200
76 | end
77 |
78 | it 'does not redirect https requests' do
79 | @request.env['HTTPS'] = 'on'
80 | get :show, id: 'abc-123'
81 | expect(response.status).to eq 200
82 | end
83 | end
84 |
85 | context 'when ssl_enabled is true' do
86 | before do
87 | allow(Settings).to receive(:ssl_enabled).and_return(true)
88 | end
89 |
90 | it 'redirects http requests to https' do
91 | @request.env['HTTPS'] = nil
92 | get :show, id: 'abc-123'
93 | expect(response).to redirect_to("https://#{request.host}#{request.path_info}")
94 | end
95 |
96 | it 'does not redirect https requests' do
97 | @request.env['HTTPS'] = 'on'
98 | get :show, id: 'abc-123', ssl: true
99 | expect(response.status).to eq 200
100 | end
101 | end
102 | end
103 |
104 | describe 'verifying that the user has approved the necessary scopes' do
105 | let(:uaa_session) { double(UaaSession, auth_header: 'bearer ', refresh_token: 'new_refresh_token') }
106 | let(:all_scopes) { ['openid', 'cloud_controller_service_permissions.read'] }
107 | let(:missing_scopes) { ['openid'] }
108 |
109 | before do
110 | session[:uaa_user_id] = 'some-user-id'
111 | session[:uaa_access_token] = ''
112 | session[:uaa_refresh_token] = ''
113 | session[:last_seen] = Time.now
114 | session[:has_retried] = has_retried
115 |
116 | allow(UaaSession).to receive(:build).with('', '').and_return(uaa_session)
117 |
118 | allow(uaa_session).to receive(:access_token).and_return('new_access_token')
119 | allow(CF::UAA::TokenCoder).to receive(:decode).with('new_access_token', verify: false).and_return({'scope' => scopes} )
120 |
121 | allow(Configuration).to receive(:manage_user_profile_url).and_return('login.com/profile')
122 | end
123 |
124 |
125 | context 'when the user has not approved the necessary scopes' do
126 | let(:scopes) { missing_scopes }
127 | let(:has_retried) { 'true' }
128 |
129 | it 'renders the approval errors page' do
130 | get :show, id: 'abc-123'
131 |
132 | expect(response.status).to eq 200
133 | expect(response.body).to include('This application requires the following permissions')
134 | end
135 | end
136 |
137 | context 'when the user updates his approvals to include the necessary scopes' do
138 | context 'the first attempt that fails' do
139 | let(:scopes) { missing_scopes }
140 | let(:has_retried) { nil }
141 |
142 | it 'redirects to the auth endpoint' do
143 | get :show, id: 'abc-123'
144 |
145 | expect(response).to redirect_to '/manage/auth/cloudfoundry'
146 | end
147 | end
148 | end
149 | end
150 |
151 | context 'when the user is not authenticated' do
152 | it 'stores the instance id in the session and redirects to the auth endpoint' do
153 | get :show, id: 'abc-123'
154 | expect(session[:instance_id]).to eql('abc-123')
155 | expect(response.status).to eql(302)
156 | expect(response).to redirect_to('/manage/auth/cloudfoundry')
157 | end
158 | end
159 |
160 | context 'when the user is authenticated' do
161 | let(:query) { double(ServiceInstanceUsageQuery) }
162 | let(:instance) { ServiceInstance.new(guid: 'abc-123') }
163 | let(:uaa_session) { double(UaaSession, auth_header: 'bearer ') }
164 |
165 | before do
166 | instance.save
167 | allow(ServiceInstanceUsageQuery).to receive(:new).and_return(query)
168 | allow(query).to receive(:execute).and_return(10.3)
169 |
170 | session[:uaa_user_id] = 'some-user-id'
171 | session[:uaa_access_token] = ''
172 | session[:uaa_refresh_token] = ''
173 | session[:last_seen] = Time.now
174 |
175 | allow(UaaSession).to receive(:build).with('', '').and_return(uaa_session)
176 |
177 | allow(uaa_session).to receive(:access_token).and_return('new_access_token')
178 | allow(uaa_session).to receive(:refresh_token).and_return('new_refresh_token')
179 |
180 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?)
181 | end
182 |
183 | after { instance.destroy }
184 |
185 | it 'updates the last_seen' do
186 | expect {
187 | get(:show, id: 'abc-123')
188 | }.to change { session[:last_seen] }
189 | end
190 |
191 | context 'when the user has permissions to manage the instance' do
192 | before do
193 | allow(Settings).to receive(:cc_api_uri) { 'http://api.example.com' }
194 |
195 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?).
196 | with('abc-123', anything).
197 | and_return(true)
198 | end
199 |
200 | it 'updates the uaa access token' do
201 | get :show, id: 'abc-123'
202 |
203 | expect(session[:uaa_access_token]).to eql('new_access_token')
204 | end
205 |
206 | it 'updates the uaa refresh token' do
207 | get :show, id: 'abc-123'
208 |
209 | expect(session[:uaa_refresh_token]).to eql('new_refresh_token')
210 | end
211 |
212 | it 'displays the usage information for the given instance' do
213 | quota = instance.max_storage_mb
214 |
215 | get :show, id: 'abc-123'
216 |
217 | expect(response.status).to eql(200)
218 | expect(response.body).to match(/10\.3 MB of #{quota} MB used./)
219 | expect(query).to have_received(:execute).once
220 | end
221 |
222 | context 'when the user is over the quota' do
223 | before do
224 | allow(query).to receive(:execute).and_return(120)
225 | end
226 | it 'displays a warning' do
227 | get :show, id: 'abc-123'
228 | expect(response.body).to include("Warning:")
229 | end
230 | end
231 | end
232 |
233 | context 'when the user does not have permission to manage the instance' do
234 | before do
235 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?).
236 | with('abc-123', anything).
237 | and_return(false)
238 | end
239 |
240 | it 'displays a "not authorized" message' do
241 | get :show, id: 'abc-123'
242 | expect(response.status).to eql(200)
243 | expect(response.body).to match(/Not\ Authorized/)
244 | expect(query).not_to have_received(:execute)
245 | end
246 | end
247 | end
248 | end
249 | end
250 |
--------------------------------------------------------------------------------
/spec/controllers/previews_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe V2::PreviewsController do
4 | describe '#show' do
5 | it 'returns a 400' do
6 | get :show
7 |
8 | expect(response.code).to eq("400")
9 | end
10 |
11 | context 'when Rails.env is development' do
12 | before do
13 | allow(Rails).to receive_messages(env: ActiveSupport::StringInquirer.new("development"))
14 | end
15 |
16 | it 'renders a view that helps developers make CSS/HTML changes' do
17 | get :show
18 |
19 | expect(response).to render_template 'manage/instances/show'
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/controllers/v2/catalogs_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe V2::CatalogsController do
4 | describe '#show' do
5 | let(:make_request) { get :show }
6 |
7 | it_behaves_like 'a controller action that requires basic auth'
8 |
9 | context 'when the basic-auth credentials are correct' do
10 | before { authenticate }
11 |
12 | it_behaves_like 'a controller action that does not log its request and response headers and body'
13 |
14 | it 'builds services from the values in Settings' do
15 | service_setting_1_stub = double(:service_setting_1_stub)
16 | service_setting_2_stub = double(:service_setting_2_stub)
17 | service_1 = double(:service_1, to_hash: {'service1' => 'to_hash'})
18 | service_2 = double(:service_1, to_hash: {'service2' => 'to_hash'})
19 | allow(Settings).to receive(:[]).with('services').
20 | and_return([service_setting_1_stub, service_setting_2_stub])
21 | expect(Service).to receive(:build).with(service_setting_1_stub).and_return(service_1)
22 | expect(Service).to receive(:build).with(service_setting_2_stub).and_return(service_2)
23 |
24 | make_request
25 |
26 | expect(response.status).to eq(200)
27 | expect(JSON.parse(response.body)).to eq(
28 | {'services' => [
29 | {'service1' => 'to_hash'},
30 | {'service2' => 'to_hash'},
31 | ]}
32 | )
33 | end
34 |
35 | context 'with invalid catalog data' do
36 | before do
37 | allow(Settings).to receive(:[]).with('services').and_return(nil)
38 | end
39 |
40 | it_behaves_like 'a controller action that does not log its request and response headers and body'
41 |
42 | context 'when there are no services' do
43 | it 'produces an empty catalog' do
44 | make_request
45 |
46 | expect(response.status).to eq(200)
47 | catalog = JSON.parse(response.body)
48 |
49 | services = catalog.fetch('services')
50 | expect(services).to be_empty
51 | end
52 | end
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/controllers/v2/service_bindings_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe V2::ServiceBindingsController do
4 | let(:db_settings) { Rails.configuration.database_configuration[Rails.env] }
5 | let(:admin_user) { db_settings.fetch('username') }
6 | let(:admin_password) { db_settings.fetch('password') }
7 | let(:database_host) { db_settings.fetch('host') }
8 | let(:database_port) { db_settings.fetch('port') }
9 |
10 | let(:instance_guid) { 'instance-1' }
11 | let(:instance) { ServiceInstance.new(guid: instance_guid, db_name: database) }
12 | let(:database) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid) }
13 |
14 | before do
15 | authenticate
16 | instance.save
17 |
18 | allow(Database).to receive(:exists?).with(database).and_return(true)
19 | allow(Settings).to receive(:allow_table_locks).and_return(true)
20 | end
21 |
22 | after { instance.destroy }
23 |
24 | describe '#update' do
25 | let(:binding_id) { '123' }
26 | let(:generated_dbname) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid) }
27 |
28 | let(:generated_username) { ServiceBinding.new(id: binding_id).username }
29 | let(:generated_password) { 'generatedpw' }
30 |
31 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid }
32 |
33 | before { allow(SecureRandom).to receive(:base64).and_return(generated_password, 'notthepassword') }
34 | after { ServiceBinding.new(id: binding_id, service_instance: instance).destroy }
35 |
36 | it_behaves_like 'a controller action that requires basic auth'
37 |
38 | it_behaves_like 'a controller action that does not log its request and response headers and body'
39 |
40 | context 'when the service instance exists' do
41 | it 'grants permission to access the given database' do
42 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(false)
43 |
44 | make_request
45 |
46 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(true)
47 | end
48 |
49 | it 'returns a 201' do
50 | make_request
51 |
52 | expect(response.status).to eq(201)
53 | end
54 |
55 | it 'responds with generated credentials' do
56 | make_request
57 |
58 | binding = JSON.parse(response.body)
59 | expect(binding['credentials']).to eq(
60 | 'hostname' => database_host,
61 | 'name' => generated_dbname,
62 | 'username' => generated_username,
63 | 'password' => generated_password,
64 | 'port' => database_port,
65 | 'jdbcUrl' => "jdbc:mysql://#{database_host}:#{database_port}/#{generated_dbname}?user=#{generated_username}&password=#{generated_password}",
66 | 'uri' => "mysql://#{generated_username}:#{generated_password}@#{database_host}:#{database_port}/#{generated_dbname}?reconnect=true",
67 | )
68 | end
69 |
70 | context 'when the read-only parameter is set to the boolean value true' do
71 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid, parameters: {'read-only' => true} }
72 | before { allow(ServiceBinding).to receive(:new).and_call_original }
73 |
74 | it 'creates a binding with read_only: true' do
75 | make_request
76 |
77 | expect(ServiceBinding).to have_received(:new).with(id: binding_id, service_instance: instance_of(ServiceInstance), read_only: true)
78 | end
79 | end
80 |
81 | context 'when the read-only parameter is not set' do
82 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid }
83 | before { allow(ServiceBinding).to receive(:new).and_call_original }
84 |
85 | it 'creates a binding with default read_only: false' do
86 | make_request
87 |
88 | expect(ServiceBinding).to have_received(:new).with(id: binding_id, service_instance: instance_of(ServiceInstance), read_only: false)
89 | end
90 | end
91 |
92 | context 'when the read-only parameter has a non-boolean value' do
93 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid, parameters: {'read-only' => 'true'} }
94 |
95 | it 'does not create a binding' do
96 | make_request
97 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(false)
98 | end
99 |
100 | it 'returns a 400 and an error message' do
101 | make_request
102 |
103 | expect(response.status).to eq(400)
104 | expect(JSON.parse(response.body)).to eq({
105 | "error" => "Error creating service binding",
106 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.",
107 | })
108 | end
109 | end
110 |
111 | context 'when an invalid parameter is provided' do
112 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid, parameters: {'unexpected-parameter' => true} }
113 |
114 | it 'does not create a binding' do
115 | make_request
116 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(false)
117 | end
118 |
119 | it 'returns a 400 and an error message' do
120 | make_request
121 |
122 | expect(response.status).to eq(400)
123 | expect(JSON.parse(response.body)).to eq({
124 | "error" => "Error creating service binding",
125 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.",
126 | })
127 | end
128 | end
129 | end
130 |
131 | context 'when the service instance does not exist' do
132 | let(:make_request) { put :update, id: binding_id, service_instance_id: 'non-existent-guid' }
133 |
134 | it 'returns a 404' do
135 | make_request
136 |
137 | expect(response.status).to eq(404)
138 | end
139 | end
140 | end
141 |
142 | describe '#destroy' do
143 | let(:binding_id) { 'BINDING-1' }
144 | let(:binding) { ServiceBinding.new(id: binding_id, service_instance: instance) }
145 | let(:username) { binding.username }
146 |
147 | let(:make_request) { delete :destroy, service_instance_id: instance.id, id: binding.id }
148 |
149 | it_behaves_like 'a controller action that requires basic auth'
150 |
151 | context 'when the binding exists' do
152 | before { binding.save }
153 | after { binding.destroy }
154 |
155 | it_behaves_like 'a controller action that does not log its request and response headers and body'
156 |
157 | it 'destroys the binding' do
158 | expect(ServiceBinding.exists?(id: binding.id, service_instance_guid: instance.guid)).to eq(true)
159 |
160 | make_request
161 |
162 | expect(ServiceBinding.exists?(id: binding.id, service_instance_guid: instance.guid)).to eq(false)
163 | end
164 |
165 | it 'returns a 200' do
166 | make_request
167 |
168 | expect(response.status).to eq(200)
169 | expect(response.body).to eq('{}')
170 | end
171 | end
172 |
173 | context 'when the binding does not exist' do
174 | it_behaves_like 'a controller action that does not log its request and response headers and body'
175 |
176 | it 'returns a 410' do
177 | make_request
178 |
179 | expect(response.status).to eq(410)
180 | end
181 | end
182 | end
183 | end
184 |
--------------------------------------------------------------------------------
/spec/controllers/v2/service_instances_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe V2::ServiceInstancesController do
4 | let(:instance_id) { '88f6fa22-c8b7-4cdc-be3a-dc09ea7734db' }
5 |
6 | before { authenticate }
7 |
8 | # this is actually the create
9 | describe '#update' do
10 | let(:max_storage_mb) { 5 }
11 | let(:db_name) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_id) }
12 | let(:services) do
13 | [
14 | {
15 | 'id' => 'foo',
16 | 'name' => 'bar',
17 | 'description' => 'desc',
18 | 'bindable' => true,
19 | 'plans' => [
20 | {
21 | 'id' => 'plan_id',
22 | 'name' => 'plan_name',
23 | 'description' => 'desc',
24 | 'max_storage_mb' => 5,
25 | },
26 | {
27 | 'id' => 'new-plan-guid',
28 | 'name' => 'plan_name',
29 | 'description' => 'desc',
30 | 'max_storage_mb' => 12,
31 | }
32 | ]
33 | }
34 | ]
35 | end
36 | let(:plan_id) { 'plan_id' }
37 | let(:make_request) do
38 | put :update, {
39 | id: instance_id,
40 | plan_id: plan_id
41 | }
42 | end
43 |
44 | before do
45 | allow(Settings).to receive(:[]).with('services').and_return(services)
46 | allow(Settings).to receive(:[]).with('ssl_enabled').and_return(true)
47 | allow(ServiceCapacity).to receive(:can_allocate?).with(max_storage_mb).and_return(true)
48 | end
49 |
50 | it_behaves_like 'a controller action that requires basic auth'
51 |
52 | it_behaves_like 'a controller action that does not log its request and response headers and body'
53 |
54 | context 'when ssl is set to false' do
55 | before do
56 | allow(Settings).to receive(:[]).with('ssl_enabled').and_return(false)
57 | end
58 |
59 | it 'returns a dashboard URL without https' do
60 | make_request
61 |
62 | instance = JSON.parse(response.body)
63 | expect(instance).to eq({'dashboard_url' => "http://p-mysql.bosh-lite.com/manage/instances/#{instance_id}"})
64 | end
65 | end
66 |
67 | context 'when the provided plan_id is not present in the catalog' do
68 | let(:plan_id) { "does-not-exist-in-catalog" }
69 |
70 | it 'does not attempt to create a service instance' do
71 | expect(ServiceInstanceManager).not_to receive(:create)
72 | make_request
73 | end
74 |
75 | it 'returns a 422 status code with a descriptive error message' do
76 | make_request
77 |
78 | expect(response.status).to eq(422)
79 | body = JSON.parse(response.body)
80 | expect(body['description']).to match /Cannot create a service instance. Plan does-not-exist-in-catalog was not found in the catalog./
81 | end
82 | end
83 |
84 | context 'when creating additional instances is allowed' do
85 | before do
86 | allow(ServiceCapacity).to receive(:can_allocate?).with(max_storage_mb) { true }
87 | end
88 |
89 | it 'returns a 201' do
90 | make_request
91 | expect(response.status).to eq(201)
92 | end
93 |
94 | it 'tells the ServiceInstanceManager to create an instance with the correct attributes' do
95 | expect(ServiceInstanceManager).to receive(:create).with({
96 | guid: instance_id,
97 | plan_guid: plan_id
98 | }).and_return(ServiceInstance.new(guid: instance_id, plan_guid: plan_id, max_storage_mb: max_storage_mb, db_name: db_name))
99 |
100 | make_request
101 | end
102 |
103 | it 'returns the dashboard_url' do
104 | make_request
105 |
106 | instance = JSON.parse(response.body)
107 | expect(instance).to eq({'dashboard_url' => "https://p-mysql.bosh-lite.com/manage/instances/#{instance_id}"})
108 | end
109 | end
110 |
111 | context 'when creating additional instances is not allowed' do
112 | before do
113 | allow(ServiceCapacity).to receive(:can_allocate?).with(max_storage_mb) { false }
114 | end
115 |
116 | it 'returns a 507' do
117 | make_request
118 |
119 | expect(response.status).to eq(507)
120 | response_json = JSON.parse(response.body)
121 | expect(response_json['description']).to eq('Service capacity has been reached')
122 | end
123 |
124 | it 'does not attempt to create a service instance' do
125 | expect(ServiceInstanceManager).not_to receive(:create)
126 | make_request
127 | end
128 | end
129 | end
130 |
131 | describe '#set_plan' do
132 | let(:plan_id) { 'new-plan-guid' }
133 | let(:make_request) { patch :set_plan, id: instance_id, plan_id: plan_id }
134 |
135 | before do
136 | allow(ServiceInstanceManager).to receive(:set_plan)
137 |
138 | request_body = {plan_id: 'new-plan-guid'}.to_json
139 | request.env['RAW_POST_DATA'] = request_body
140 | end
141 |
142 | it_behaves_like 'a controller action that requires basic auth'
143 |
144 | it_behaves_like 'a controller action that does not log its request and response headers and body'
145 |
146 | context 'when the service instance exists' do
147 | before do
148 | ServiceInstance.create(guid: instance_id, plan_guid: 'some-plan-guid')
149 | end
150 |
151 | it 'returns a 200' do
152 | make_request
153 |
154 | expect(response.status).to eq(200)
155 | body = JSON.parse(response.body)
156 | expect(body).to eq({})
157 | end
158 |
159 | it 'tells the service instance manager to change the plan of the instance' do
160 | expect(ServiceInstanceManager).to receive(:set_plan).with({
161 | guid: instance_id,
162 | plan_guid: 'new-plan-guid'
163 | })
164 |
165 | make_request
166 | end
167 | end
168 |
169 | context 'when the service instance does not exist' do
170 | before do
171 | allow(ServiceInstanceManager).to receive(:set_plan).with({
172 | guid: instance_id,
173 | plan_guid: 'new-plan-guid'
174 | }).and_raise(ServiceInstanceManager::ServiceInstanceNotFound)
175 | end
176 |
177 | it 'returns a 404' do
178 | make_request
179 |
180 | expect(response.status).to eq(404)
181 | expect(response.body).to eq '{"description":"Service instance not found"}'
182 | end
183 | end
184 |
185 | context 'when the service plan does not exist' do
186 | before do
187 | allow(ServiceInstanceManager).to receive(:set_plan).with({
188 | guid: instance_id,
189 | plan_guid: 'new-plan-guid'
190 | }).and_raise(ServiceInstanceManager::ServicePlanNotFound)
191 | end
192 |
193 | it 'returns a 400' do
194 | make_request
195 |
196 | expect(response.status).to eq(400)
197 | expect(response.body).to eq '{"description":"Service plan not found"}'
198 | end
199 | end
200 |
201 | context 'when the service plan cannot be updated' do
202 | before do
203 | allow(ServiceInstanceManager).to receive(:set_plan).and_raise(ServiceInstanceManager::InvalidServicePlanUpdate.new('cannot downgrade'))
204 | end
205 |
206 | it 'returns a 422 and forwards the error message' do
207 | make_request
208 |
209 | expect(response.status).to eq 422
210 | expect(response.body).to eq '{"description":"cannot downgrade"}'
211 | end
212 | end
213 | end
214 |
215 | describe '#destroy' do
216 | let(:make_request) { delete :destroy, id: instance_id }
217 |
218 | it_behaves_like 'a controller action that requires basic auth'
219 |
220 | it_behaves_like 'a controller action that does not log its request and response headers and body'
221 |
222 | context 'when the service instance exists' do
223 | before do
224 | ServiceInstance.create(guid: instance_id, plan_guid: 'some-plan-guid')
225 | end
226 |
227 | it 'returns a 200' do
228 | make_request
229 |
230 | expect(response.status).to eq(200)
231 | body = JSON.parse(response.body)
232 | expect(body).to eq({})
233 | end
234 |
235 | it 'tells the service instance manager to destroy the instance' do
236 | expect(ServiceInstanceManager).to receive(:destroy).with({
237 | guid: instance_id
238 | })
239 |
240 | make_request
241 | end
242 | end
243 |
244 | context 'when the service instance does not exist' do
245 | it 'returns a 410' do
246 | expect(ServiceInstanceManager).to receive(:destroy).with({
247 | guid: instance_id
248 | }).and_raise(ServiceInstanceManager::ServiceInstanceNotFound)
249 |
250 | make_request
251 |
252 | expect(response.status).to eq(410)
253 | end
254 | end
255 | end
256 | end
257 |
--------------------------------------------------------------------------------
/spec/features/plan_update_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Plan Upgrade' do
4 |
5 | let(:instance_id_0) { SecureRandom.uuid }
6 | let(:binding_id_0) { SecureRandom.uuid }
7 | let(:plan_0) { Settings.services[0].plans[0] }
8 | let(:plan_1) { Settings.services[0].plans[1] }
9 | let(:max_storage_mb_0) { plan_0.max_storage_mb.to_i }
10 | let(:max_storage_mb_1) { plan_1.max_storage_mb.to_i }
11 |
12 | before do
13 | allow(Settings).to receive(:allow_table_locks).and_return(true)
14 | end
15 |
16 | after do
17 | delete "/v2/service_instances/#{instance_id_0}/service_bindings/#{binding_id_0}"
18 | delete "/v2/service_instances/#{instance_id_0}"
19 | end
20 |
21 | specify 'User updates to a plan with a larger quota' do
22 | put "/v2/service_instances/#{instance_id_0}", {plan_id: plan_0.id}
23 | put "/v2/service_instances/#{instance_id_0}/service_bindings/#{binding_id_0}"
24 | binding_0 = JSON.parse(response.body)
25 | credentials_0 = binding_0.fetch('credentials')
26 |
27 | # Fill db past the limit of plan 0
28 | client_0 = create_mysql_client(credentials_0)
29 | create_table_and_write_data(client_0, max_storage_mb_0)
30 |
31 | # Change instance to plan 1
32 | patch "/v2/service_instances/#{instance_id_0}", { plan_id: plan_1.id, previous_values: {} }
33 | expect(response.status).to eq 200
34 |
35 | # Verify that we can write
36 | client_0 = create_mysql_client(credentials_0)
37 | verify_client_can_write(client_0)
38 | end
39 |
40 | specify 'User tries to downgrade to a plan with a smaller quota than he is currently using' do
41 | # Create db with larger quota
42 | put "/v2/service_instances/#{instance_id_0}", {plan_id: plan_1.id}
43 | put "/v2/service_instances/#{instance_id_0}/service_bindings/#{binding_id_0}"
44 | binding_0 = JSON.parse(response.body)
45 | credentials_0 = binding_0.fetch('credentials')
46 |
47 | # Fill db past limit of plan 0
48 | client_0 = create_mysql_client(credentials_0)
49 | create_table_and_write_data(client_0, max_storage_mb_0 + 1)
50 |
51 | # Attempt to change instance to plan 0
52 | patch "/v2/service_instances/#{instance_id_0}", { plan_id: plan_0.id, previous_values: {} }
53 | expect(response.status).to eq 422
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/lib/configuration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Configuration do
4 |
5 | before do
6 | Configuration.clear
7 | stub_request(:any, "#{Settings.cc_api_uri}/info").
8 | to_return(body: JSON.generate({
9 | name: 'vcap',
10 | build: '2222',
11 | support: 'http://support.cloudfoundry.com',
12 | version: 2,
13 | description: 'Cloud Foundry sponsored by Pivotal',
14 | authorization_endpoint: 'http://login.bosh-lite.com',
15 | token_endpoint: 'https://uaa.bosh-lite.com',
16 | allow_debug: true
17 | }))
18 | end
19 |
20 | describe '#auth_server_url' do
21 | it 'uses the cc_api_uri to get the url for the auth server' do
22 | expect(Configuration.auth_server_url).to eql('http://login.bosh-lite.com')
23 | expect(a_request(:get, "#{Settings.cc_api_uri}/info")).to have_been_made
24 | end
25 | end
26 |
27 | describe '#auth_server_logout_url' do
28 | it 'uses the cc_api_uri to return the auth server logout url' do
29 | expect(Configuration.auth_server_logout_url).to eql('http://login.bosh-lite.com/logout.do')
30 | end
31 | end
32 |
33 | describe '#token_server_url' do
34 | it 'uses the cc_api_uri to get the url for the token server' do
35 | expect(Configuration.token_server_url).to eql('https://uaa.bosh-lite.com')
36 | expect(a_request(:get, "#{Settings.cc_api_uri}/info")).to have_been_made
37 | end
38 | end
39 |
40 | describe '#documentation_url' do
41 | it 'uses the documentationUrl of the first service in the catalog' do
42 | expect(Configuration.documentation_url).to eql('http://docs.run.pivotal.io')
43 | end
44 |
45 | context 'when the catalog is empty' do
46 | before do
47 | allow(Settings).to receive(:services).and_return([])
48 | end
49 |
50 | it 'is nil' do
51 | expect(Configuration.documentation_url).to be_nil
52 | end
53 | end
54 | end
55 |
56 | describe '#support_url' do
57 | it 'uses the supportUrl of the first service in the catalog' do
58 | expect(Configuration.support_url).to eql('http://support.run.pivotal.io/home')
59 | end
60 |
61 | context 'when the catalog is empty' do
62 | before do
63 | allow(Settings).to receive(:services).and_return([])
64 | end
65 |
66 | it 'is nil' do
67 | expect(Configuration.support_url).to be_nil
68 | end
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/lib/request_response_logger_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe RequestResponseLogger do
4 | describe "#log_headers_and_body" do
5 | let(:fake_rails_logger) { double(:fake_rails_logger) }
6 | let(:request_response_logger) { described_class.new("Message:", fake_rails_logger) }
7 |
8 | let(:headers) {
9 | {
10 | "CONTENT_TYPE" => 'application/json',
11 | "HTTP_AUTHORIZATION" => 'basic: some-auth-token',
12 | "THIS_KEY_SHOULD_NOT_BE_LOGGED" => 'unknown'
13 | }
14 | }
15 |
16 | it 'logs the request headers and body' do
17 | expect(fake_rails_logger).to receive(:info) do |log_message|
18 | json_log_message = log_message.sub(/^\s+Message:\s+/, "")
19 |
20 | request_info = JSON.parse(json_log_message)
21 | expect(request_info['body']).to eq "body"
22 | expect(request_info['headers']["CONTENT_TYPE"]).to eq "application/json"
23 | end
24 |
25 | request_response_logger.log_headers_and_body(headers, "body")
26 | end
27 |
28 | it 'filters out sensitive data headers' do
29 | expect(fake_rails_logger).to receive(:info) do |log_message|
30 | json_log_message = log_message.sub(/^\s+Message:\s+/, "")
31 |
32 | request_info = JSON.parse(json_log_message)
33 | expect(request_info['headers']["HTTP_AUTHORIZATION"]).not_to match "some-auth-token"
34 | end
35 |
36 | request_response_logger.log_headers_and_body(headers, "body")
37 | end
38 |
39 | it 'does not log unknown headers' do
40 | expect(fake_rails_logger).to receive(:info) do |log_message|
41 | json_log_message = log_message.sub(/^\s+Message:\s+/, "")
42 |
43 | request_info = JSON.parse(json_log_message)
44 | expect(request_info['headers']).not_to have_key("THIS_KEY_SHOULD_NOT_BE_LOGGED")
45 | end
46 |
47 | request_response_logger.log_headers_and_body(headers, "body")
48 | end
49 |
50 | context 'when log_all_headers is true' do
51 | it 'filters out sensitive data headers' do
52 | expect(fake_rails_logger).to receive(:info) do |log_message|
53 | json_log_message = log_message.sub(/^\s+Message:\s+/, "")
54 |
55 | request_info = JSON.parse(json_log_message)
56 | expect(request_info['headers']["HTTP_AUTHORIZATION"]).not_to match "some-auth-token"
57 | end
58 |
59 | request_response_logger.log_headers_and_body(headers, "body", true)
60 | end
61 |
62 | it 'logs unknown headers' do
63 | expect(fake_rails_logger).to receive(:info) do |log_message|
64 | json_log_message = log_message.sub(/^\s+Message:\s+/, "")
65 |
66 | request_info = JSON.parse(json_log_message)
67 | expect(request_info['headers']).to have_key("THIS_KEY_SHOULD_NOT_BE_LOGGED")
68 | end
69 |
70 | request_response_logger.log_headers_and_body(headers, "body", true)
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/spec/lib/service_capacity_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ServiceCapacity do
4 | describe '.can_allocate?' do
5 | before do
6 | allow(Settings).to receive(:[]).with('persistent_disk').and_return(1000)
7 | allow(Settings).to receive(:[]).with('gcache_size').and_return(10)
8 | allow(Settings).to receive(:[]).with('ib_log_file_size').and_return(5)
9 | allow(ServiceInstance).to receive(:reserved_space_in_mb).and_return(900)
10 | allow(ServiceInstance).to receive_message_chain(:all, :count).and_return(5)
11 | end
12 |
13 | it 'returns true when the allocated space + requested space is < the storage capacity' do
14 | expect(described_class.can_allocate?(15)).to eq true
15 | end
16 |
17 | it 'returns true when the allocated space + requested space is = the storage capacity' do
18 | expect(described_class.can_allocate?(15.947)).to eq true
19 | end
20 |
21 | it 'returns false when the allocated space + requested space is > the storage capacity' do
22 | expect(described_class.can_allocate?(16)).to eq false
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/lib/service_instance_manager_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ServiceInstanceManager do
4 | let(:instance_id) { '88f6fa22-c8b7-4cdc-be3a-dc09ea7734db' }
5 | let(:database_name) { 'cf_88f6fa22_c8b7_4cdc_be3a_dc09ea7734db' }
6 | let(:plan_id) { '8888-ffff' }
7 | let(:non_existent_plan_id) { 'non-existent-guid' }
8 | let(:max_storage_mb) { 300 }
9 |
10 | before do
11 | allow(Catalog).to receive(:has_plan?).with(plan_id).and_return(true)
12 | allow(Catalog).to receive(:has_plan?).with(non_existent_plan_id).and_return(false)
13 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_id).and_return(max_storage_mb)
14 | end
15 |
16 | describe '.database_name_from_service_instance_guid' do
17 | it 'converts instance_id to database_name' do
18 | expect(ServiceInstanceManager.database_name_from_service_instance_guid(instance_id)).to eq(database_name)
19 | end
20 | end
21 |
22 | describe '.create' do
23 | after {
24 | Database.drop(database_name)
25 | }
26 |
27 | it 'saves a ServiceInstance in the broker database' do
28 | expect { described_class.create(guid: instance_id, plan_guid: plan_id) }.
29 | to change(ServiceInstance, :count).from(0).to(1)
30 | expect(ServiceInstance.last.guid).to eq(instance_id)
31 | expect(ServiceInstance.last.plan_guid).to eq(plan_id)
32 | expect(ServiceInstance.last.max_storage_mb).to eq (max_storage_mb)
33 | expect(ServiceInstance.last.db_name).to eq (database_name)
34 | end
35 |
36 | it 'creates a new MySQL database' do
37 | described_class.create(guid: instance_id, plan_guid: plan_id)
38 | expect(Database.exists?(database_name)).to eq true
39 | end
40 |
41 | context 'when creating the MySQL database fails' do
42 | before do
43 | expect(Database).to receive(:create).and_raise(ActiveRecord::ActiveRecordError)
44 | end
45 |
46 | it 'does not save a ServiceInstance in the broker database' do
47 | expect {
48 | begin
49 | described_class.create(guid: instance_id, plan_guid: plan_id)
50 | rescue ActiveRecord::ActiveRecordError
51 | end
52 | }.not_to change(ServiceInstance, :count)
53 | end
54 | end
55 |
56 | context 'when the plan guid is not in the catalog' do
57 |
58 | it 'raises an error' do
59 | expect {
60 | described_class.create(guid: instance_id, plan_guid: non_existent_plan_id)
61 | }. to raise_error(RuntimeError, "Plan #{non_existent_plan_id} was not found in the catalog.")
62 | end
63 |
64 | it 'does not save a ServiceInstance in the broker database' do
65 | expect {
66 | begin
67 | described_class.create(guid: instance_id, plan_guid: non_existent_plan_id)
68 | rescue RuntimeError
69 | end
70 | }.not_to change(ServiceInstance, :count)
71 | end
72 |
73 | it 'does not try to create a database' do
74 | expect(Database).not_to receive(:create)
75 | begin
76 | described_class.create(guid: instance_id, plan_guid: non_existent_plan_id)
77 | rescue RuntimeError
78 | end
79 | end
80 | end
81 |
82 | context 'when the instance guid is of the wrong format' do
83 | it 'raises an error' do
84 | expect {
85 | described_class.create(guid: 'Very$%$%#$BAD--__,,guid', plan_guid: plan_id)
86 | }.to raise_error(RuntimeError, 'Only GUIDs matching [0-9,a-z,A-Z$-]+ are allowed')
87 | end
88 |
89 | it 'does not save a ServiceInstance in the broker database' do
90 | expect {
91 | begin
92 | described_class.create(guid: 'Very$%$%#$BAD--__,,guid', plan_guid: plan_id)
93 | rescue RuntimeError
94 | end
95 | }.not_to change(ServiceInstance, :count)
96 | end
97 |
98 | it 'does not try to create a database' do
99 | expect(Database).not_to receive(:create)
100 | begin
101 | described_class.create(guid: 'Very$%$%#$BAD--__,,guid', plan_guid: plan_id)
102 | rescue RuntimeError
103 | end
104 | end
105 | end
106 | end
107 |
108 | describe '.set_plan' do
109 | let(:new_plan_id) { 'new-plan-id' }
110 | let!(:service_instance) { described_class.create(guid: instance_id, plan_guid: plan_id) }
111 |
112 | before do
113 | allow(Catalog).to receive(:has_plan?).with(new_plan_id).and_return(true)
114 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(new_plan_id).and_return(12)
115 | end
116 |
117 | it 'changes the plan_guid' do
118 | described_class.set_plan(guid: instance_id, plan_guid: new_plan_id)
119 | service_instance.reload
120 | expect(service_instance.plan_guid).to eq new_plan_id
121 | expect(service_instance.max_storage_mb).to eq 12
122 | end
123 |
124 | context 'when there is no plan with the given guid' do
125 | it 'raises a ServiceInstanceManager::ServicePlanNotFound error' do
126 | expect { described_class.set_plan(guid: instance_id, plan_guid: non_existent_plan_id) }.to raise_error(ServiceInstanceManager::ServicePlanNotFound)
127 | end
128 | end
129 |
130 | context 'when there is no instance with the given guid' do
131 | let!(:service_instance) { nil }
132 | it 'raises a ServiceInstanceManager::ServiceInstanceNotFound error' do
133 | expect { described_class.set_plan(guid: instance_id, plan_guid: new_plan_id) }.to raise_error(ServiceInstanceManager::ServiceInstanceNotFound)
134 | end
135 | end
136 |
137 | context 'when downgrading would put the databases over the quota limit of its new plan' do
138 | before do
139 | db_name = ServiceInstanceManager.database_name_from_service_instance_guid(instance_id)
140 | allow(Database).to receive(:usage).with(db_name).and_return 30
141 | end
142 |
143 | it 'raises an InvalidServicePlanUpdate error' do
144 | expect{ServiceInstanceManager.set_plan(guid: instance_id, plan_guid: new_plan_id)}.to raise_error(ServiceInstanceManager::InvalidServicePlanUpdate)
145 | end
146 | end
147 | end
148 |
149 | describe '.destroy' do
150 | context 'when there is an instance with the given guid' do
151 | before do
152 | described_class.create(guid: instance_id, plan_guid: plan_id)
153 | end
154 |
155 | it 'removes the ServiceInstance from the broker database' do
156 | expect { described_class.destroy(guid: instance_id) }.
157 | to change(ServiceInstance, :count).from(1).to(0)
158 | end
159 |
160 | it 'drops the MySQL database' do
161 | expect(Database.exists?(database_name)).to eq true
162 | described_class.destroy(guid: instance_id)
163 | expect(Database.exists?(database_name)).to eq false
164 | end
165 | end
166 |
167 | context 'when there is no instance with the given guid' do
168 | it 'raises an error' do
169 | expect {
170 | described_class.destroy(guid: instance_id)
171 | }.to raise_error(ServiceInstanceManager::ServiceInstanceNotFound)
172 | end
173 |
174 | it 'does not attempt to drop any databases' do
175 | expect(Database).not_to receive(:drop)
176 | begin
177 | described_class.destroy(guid: instance_id)
178 | rescue ServiceInstanceManager::ServiceInstanceNotFound
179 | end
180 | end
181 | end
182 | end
183 |
184 | describe '.sync_service_instances' do
185 | context 'when the plan db size has changed' do
186 | it 'updates service instance plan sizes' do
187 | # create an instance of default size
188 | instance = described_class.create(guid: instance_id, plan_guid: plan_id)
189 | expect(instance.max_storage_mb).to eq max_storage_mb
190 |
191 | # increase plan size in Catalog
192 | new_plan_size = max_storage_mb + 100
193 | expect(Catalog).to receive(:plans).and_return([
194 | Plan.build('id' => plan_id,
195 | 'name' => 'plan_name',
196 | 'description' => 'plan description',
197 | 'max_storage_mb' => new_plan_size)
198 | ])
199 |
200 | # call sync_service_instances
201 | described_class.sync_service_instances
202 |
203 | # expect instance to have same guid but new plan size
204 | updated_instance = ServiceInstance.find_by(id: instance.id)
205 | expect(updated_instance.plan_guid).to eq instance.plan_guid
206 | expect(updated_instance.max_storage_mb).to eq new_plan_size
207 | end
208 | end
209 | end
210 | end
211 |
--------------------------------------------------------------------------------
/spec/lib/table_lock_manager_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 |
4 | describe TableLockManager do
5 | let(:instance_guid) {'88f6fa22-c8b7-4cdc-be3a-dc09ea7734db'}
6 | let(:username) {binding.username}
7 | let(:database) {ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid)}
8 | let(:connection) {ServiceInstance.connection}
9 |
10 | let(:instance) {ServiceInstance.new(
11 | guid: instance_guid,
12 | plan_guid: 'plan_guid',
13 | db_name: database)
14 | }
15 |
16 | let(:binding) {ServiceBinding.new(id: 'fa790aea-ab7f-41e8-b6f9-a2a1d60403f5', service_instance: instance)}
17 |
18 | before do
19 | allow(Database).to receive(:exists?).with(database).and_return(true)
20 | end
21 |
22 | after do
23 | begin
24 | connection.execute("DROP USER #{username}")
25 | rescue ActiveRecord::StatementInvalid => e
26 | raise unless e.message =~ /DROP USER failed/
27 | end
28 | end
29 |
30 | def fetch_grants
31 | grants = connection.select_values("SHOW GRANTS FOR #{username}")
32 |
33 | matching_grants = grants.select {|grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/)}
34 | end
35 |
36 | describe 'update_table_lock_permissions' do
37 | context 'when table locks are disabled' do
38 | before do
39 | allow(Settings).to receive(:allow_table_locks).and_return(true)
40 | binding.save
41 | grants = fetch_grants
42 | expect(grants.length).to eq(1)
43 | expect(grants[0]).to include("ALL PRIVILEGES")
44 |
45 | allow(Settings).to receive(:allow_table_locks).and_return(false)
46 | end
47 |
48 | it 'revokes lock table permissions on all users' do
49 | TableLockManager.update_table_lock_permissions
50 |
51 | grants = fetch_grants
52 |
53 | expect(grants.length).to eq(1)
54 | expect(grants[0]).not_to include("ALL PRIVILEGES")
55 | expect(grants[0]).not_to include("LOCK TABLES")
56 | end
57 | end
58 |
59 | context 'when table locks are enabled' do
60 | before do
61 | allow(Settings).to receive(:allow_table_locks).and_return(false)
62 | binding.save
63 | grants = fetch_grants
64 | expect(grants.length).to eq(1)
65 | expect(grants[0]).not_to include("ALL PRIVILEGES")
66 |
67 | allow(Settings).to receive(:allow_table_locks).and_return(true)
68 | end
69 |
70 | it 'grants lock table permissions on all users' do
71 | TableLockManager.update_table_lock_permissions
72 |
73 | grants = fetch_grants
74 |
75 | expect(grants.length).to eq(1)
76 | expect(grants[0]).to include("ALL PRIVILEGES")
77 | end
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/spec/lib/uaa_session_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe UaaSession do
4 | let(:access_token) { 'my_access_token' }
5 | let(:refresh_token) { 'my_refresh_token' }
6 |
7 | describe '.build' do
8 | let(:login_url) { 'http://login.example.com' }
9 | let(:uaa_url) { 'http://uaa.example.com' }
10 | let(:dashboard_client_id) { '' }
11 | let(:dashboard_client_secret) { '' }
12 |
13 | before do
14 | allow(Configuration).to receive(:auth_server_url).and_return(login_url)
15 | allow(Configuration).to receive(:token_server_url).and_return(uaa_url)
16 | allow(Settings).to receive(:services).and_return(
17 | [
18 | double(dashboard_client: double(id: dashboard_client_id, secret: dashboard_client_secret))
19 | ]
20 | )
21 | end
22 |
23 | subject { UaaSession.build(access_token, refresh_token) }
24 |
25 | context 'when the access token is not expired' do
26 | before do
27 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('exp' => 1.minute.from_now.to_i)
28 | end
29 |
30 | it 'sets the access token member' do
31 | expect(subject.access_token).to eq(access_token)
32 | end
33 |
34 | it 'sets the refresh token member' do
35 | expect(subject.refresh_token).to eq(refresh_token)
36 | end
37 |
38 | it 'returns a token that is encoded and can be used in a header' do
39 | expect(subject.auth_header).to eql('bearer my_access_token')
40 | end
41 | end
42 |
43 | context 'when the access token is expired and refreshed token is symbol' do
44 | let(:token_issuer) { double(CF::UAA::TokenIssuer, refresh_token_grant: token_info) }
45 | let(:token_info) { CF::UAA::TokenInfo.new(access_token: 'new_access_token', refresh_token: 'new_refresh_token', token_type: 'bearer') }
46 |
47 | before do
48 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('exp' => 1.minute.ago.to_i)
49 |
50 | expect(CF::UAA::TokenIssuer).to receive(:new).
51 | with(login_url, dashboard_client_id, dashboard_client_secret, { token_target: uaa_url }).
52 | and_return(token_issuer)
53 | end
54 |
55 | it 'uses the refresh token to get a new access token' do
56 | expect(subject.auth_header).to eql('bearer new_access_token')
57 | end
58 |
59 | it 'updates the access token' do
60 | expect(subject.access_token).to eql('new_access_token')
61 | end
62 |
63 | it 'updates the refresh token' do
64 | expect(subject.refresh_token).to eql('new_refresh_token')
65 | end
66 | end
67 |
68 | context 'when the access token is expired and refreshed token is string' do
69 | let(:token_issuer) { double(CF::UAA::TokenIssuer, refresh_token_grant: token_info) }
70 | let(:token_info) { CF::UAA::TokenInfo.new("access_token" => 'new_access_token', "refresh_token" => 'new_refresh_token', "token_type" => 'bearer') }
71 |
72 | before do
73 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('exp' => 1.minute.ago.to_i)
74 |
75 | expect(CF::UAA::TokenIssuer).to receive(:new).
76 | with(login_url, dashboard_client_id, dashboard_client_secret, { token_target: uaa_url }).
77 | and_return(token_issuer)
78 | end
79 |
80 | it 'uses the refresh token to get a new access token' do
81 | expect(subject.auth_header).to eql('bearer new_access_token')
82 | end
83 |
84 | it 'updates the access token' do
85 | expect(subject.access_token).to eql('new_access_token')
86 | end
87 | end
88 | end
89 |
90 | end
91 |
--------------------------------------------------------------------------------
/spec/models/catalog_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Catalog do
4 | let(:services) {
5 | [
6 | service_1_attrib,
7 | service_2_attrib
8 | ]
9 | }
10 | let(:service_1_attrib) {
11 | {
12 | 'id' => 'foo',
13 | 'name' => 'bar',
14 | 'description' => 'desc',
15 | 'bindable' => true,
16 | 'plans' => [
17 | plan_1_attrib,
18 | plan_2_attrib
19 | ]
20 | }
21 | }
22 | let(:service_2_attrib) {
23 | {
24 | 'id' => 'foo2',
25 | 'name' => 'bar2',
26 | 'description' => 'desc2',
27 | 'bindable' => true,
28 | 'plans' => [
29 | plan_3_attrib
30 | ]
31 | }
32 | }
33 | let(:plan_1_attrib) {
34 | {
35 | 'id' => 'plan_id_1',
36 | 'name' => 'plan_name_1',
37 | 'description' => 'desc1',
38 | 'max_storage_mb' => 5,
39 | 'max_user_connections' => 7,
40 | }
41 | }
42 | let(:plan_2_attrib) {
43 | {
44 | 'id' => 'plan_id_2',
45 | 'name' => 'plan_name_2',
46 | 'description' => 'desc2',
47 | 'max_storage_mb' => 100,
48 | 'max_user_connections' => 40,
49 | }
50 | }
51 | let(:plan_3_attrib) {
52 | {
53 | 'id' => 'plan_id_3',
54 | 'name' => 'plan_name_3',
55 | 'description' => 'desc3',
56 | 'max_storage_mb' => 101,
57 | 'max_user_connections' => 41,
58 | }
59 | }
60 |
61 | before do
62 | allow(Settings).to receive(:[]).with('services').and_return(services)
63 | end
64 |
65 | describe '.plans' do
66 | it 'returns an array of plan objects representing the plans in the catalog' do
67 | catalog_plans = Catalog.plans.map(&:to_hash)
68 | expected_plans = [
69 | Plan.build(plan_1_attrib),
70 | Plan.build(plan_2_attrib),
71 | Plan.build(plan_3_attrib)
72 | ].map(&:to_hash)
73 |
74 | expect(catalog_plans).to match_array expected_plans
75 | end
76 | end
77 |
78 | describe '.storage_quota_for_plan_guid' do
79 | it 'returns max_storage_mb for the plan with the specified guid' do
80 | expect(Catalog.storage_quota_for_plan_guid('plan_id_2')).to eq(100)
81 | end
82 |
83 | context 'when the plan with the guid is not found' do
84 | it 'returns nil' do
85 | expect(Catalog.storage_quota_for_plan_guid('non-existent-plan')).to be nil
86 | end
87 | end
88 | end
89 |
90 | describe '.connection_quota_for_plan_guid' do
91 | it 'returns max_user_connections for the plan with the specified guid' do
92 | expect(Catalog.connection_quota_for_plan_guid('plan_id_2')).to eq(40)
93 | end
94 |
95 | context 'when the plan with the guid is not found' do
96 | it 'returns nil' do
97 | expect(Catalog.connection_quota_for_plan_guid('non-existent-plan')).to be nil
98 | end
99 | end
100 | end
101 |
102 | describe '.has_plan?' do
103 | it 'returns true if plan_id exists in the catalog' do
104 | expect(Catalog.has_plan?('plan_id_2')).to be true
105 | end
106 |
107 | it 'returns false if plan_id does not exist in the catalog' do
108 | expect(Catalog.has_plan?('plan_id_banana')).to be false
109 | end
110 |
111 | context 'when there are no services' do
112 | let(:services) { [] }
113 |
114 | it 'returns false' do
115 | expect(Catalog.has_plan?('any-plan')).to be false
116 | end
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/spec/models/cloud_controller_http_client_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe CloudControllerHttpClient do
4 | describe '#get' do
5 | let(:client) { CloudControllerHttpClient.new(auth_header) }
6 | let(:cc_url) { 'http://api.example.com/cc' }
7 | let(:auth_header) { 'a-correct-header' }
8 | let(:response_body) { '{"something": true}' }
9 |
10 | before do
11 | allow(Settings).to receive(:cc_api_uri).and_return(cc_url)
12 |
13 | stub_request(:get, %r(#{cc_url}/.*)).
14 | to_return(body: response_body)
15 | end
16 |
17 | it 'makes a request to the correct endpoint' do
18 | client.get("/path/to/endpoint")
19 | expect(
20 | a_request(:get, "#{cc_url}/path/to/endpoint").
21 | with(headers: { 'Authorization' => auth_header })
22 | ).to have_been_made
23 | end
24 |
25 | it 'returns the parsed response body' do
26 | expect(client.get("/path/to/endpoint")).to eq(JSON.parse(response_body))
27 | end
28 |
29 | context 'when the cc_url has a trailing slash' do
30 | let(:cc_url) { 'http://api.example.com/cc/' }
31 |
32 | before do
33 | stub_request(:get, %r(#{cc_url}.*)).
34 | to_return(body: response_body)
35 | end
36 |
37 | it 'constructs the request url appropriately' do
38 | client.get("/path/to/endpoint")
39 | expect(
40 | a_request(:get, "#{cc_url}path/to/endpoint")
41 | ).to have_been_made
42 | end
43 | end
44 |
45 | context 'when the CC uri uses https' do
46 | let(:cc_url) { 'https://api.example.com/cc' }
47 |
48 | it 'makes a request to the correct endpoint' do
49 | client.get("/path/to/endpoint")
50 | expect(
51 | a_request(:get, "#{cc_url}/path/to/endpoint").
52 | with(headers: { 'Authorization' => auth_header })
53 | ).to have_been_made
54 | end
55 |
56 | describe 'ssl cert verification' do
57 | let(:http) { double(:http) }
58 | let(:response) { double(:response, body: '{}') }
59 |
60 | before do
61 | allow(Net::HTTP).to receive(:new).and_return(http)
62 | allow(http).to receive(:use_ssl=)
63 | allow(http).to receive(:verify_mode=)
64 | allow(http).to receive(:request).and_return(response)
65 | end
66 |
67 | it 'sets use_ssl to true' do
68 | client.get('/a/path')
69 | expect(http).to have_received(:use_ssl=).with(true)
70 | end
71 |
72 | context 'when skip_ssl_validation is false' do
73 | before do
74 | allow(Settings).to receive(:skip_ssl_validation).and_return(false)
75 | end
76 |
77 | it 'verifies the ssl cert' do
78 | client.get('/a/path')
79 | expect(http).to have_received(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
80 | end
81 | end
82 |
83 | context 'when skip_ssl_validation is true' do
84 | before do
85 | allow(Settings).to receive(:skip_ssl_validation).and_return(true)
86 | end
87 |
88 | it 'does not verify the ssl cert' do
89 | client.get('/a/path')
90 | expect(http).to have_received(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
91 | end
92 | end
93 | end
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/spec/models/database_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Database do
4 | let(:db_name) { 'database_name' }
5 |
6 | describe '.create' do
7 | after { connection.execute("DROP DATABASE `#{db_name}`") }
8 |
9 | it 'creates a new database' do
10 | Database.create(db_name)
11 | expect(connection.select_value("SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE schema_name='#{db_name}'")).to eq(1)
12 | end
13 |
14 | context 'when the database already exists' do
15 | before do
16 | Database.create(db_name)
17 | end
18 |
19 | it 'avoids collisions with existing databases' do
20 | expect {
21 | Database.create(db_name)
22 | }.to_not change {
23 | connection.select_values("SHOW DATABASES LIKE '#{db_name}'").count
24 | }.from(1)
25 | end
26 | end
27 | end
28 |
29 | describe '.drop' do
30 | context 'when the database exists' do
31 | before { connection.execute("CREATE DATABASE `#{db_name}`") }
32 |
33 | it 'drops the database' do
34 | expect {
35 | Database.drop(db_name)
36 | }.to change {
37 | connection.select_values("SHOW DATABASES LIKE '#{db_name}'").count
38 | }.from(1).to(0)
39 | end
40 | end
41 |
42 | context 'when the database does not exist' do
43 | it 'does not raise an error' do
44 | expect {
45 | Database.drop('unknown_database')
46 | }.to_not raise_error
47 | end
48 | end
49 | end
50 |
51 | describe '.exists?' do
52 | context 'when the database exists' do
53 | before { connection.execute("CREATE DATABASE `#{db_name}`") }
54 | after { connection.execute("DROP DATABASE `#{db_name}`") }
55 |
56 | it 'returns true' do
57 | expect(Database.exists?(db_name)).to eq(true)
58 | end
59 | end
60 |
61 | context 'when the database does not exist' do
62 | it 'returns false' do
63 | expect(Database.exists?(db_name)).to eq(false)
64 | end
65 | end
66 | end
67 |
68 | describe '.usage' do
69 | let(:mb_string) { 'a' * 1024 * 1024 }
70 | before { Database.create(db_name) }
71 | after { Database.drop(db_name) }
72 |
73 | it 'returns the data usage of the db in megabytes' do
74 | connection.execute("CREATE TABLE #{db_name}.mytable (id MEDIUMINT, data LONGTEXT)")
75 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (1, '#{mb_string}')")
76 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (2, '#{mb_string}')")
77 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (3, '#{mb_string}')")
78 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (4, '#{mb_string}')")
79 |
80 | expect(Database.usage(db_name)).to eq 4
81 | end
82 | end
83 |
84 | describe '.with_reconnect' do
85 | before do
86 | allow(Kernel).to receive(:sleep)
87 |
88 | reconnect_count = 0
89 | allow(ActiveRecord::Base.connection).to receive(:reconnect!) do
90 | reconnect_count += 1
91 | if reconnect_count == 1
92 | raise Mysql2::Error.new("fake")
93 | else
94 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
95 | end
96 | end
97 |
98 | @foo = double('bob')
99 | allow(@foo).to receive(:bar).and_raise(ActiveRecord::ActiveRecordError)
100 | end
101 |
102 | it 'attempts to reconnect every 3 seconds if the connection becomes inactive' do
103 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
104 |
105 | Database.with_reconnect do
106 | @foo.bar
107 | end
108 |
109 | expect(@foo).to have_received(:bar)
110 | expect(ActiveRecord::Base.connection).to have_received(:reconnect!).twice
111 | expect(Kernel).to have_received(:sleep).with(3.seconds)
112 | end
113 |
114 | it 'stops trying to reconnect eventually, in case there is an unrecoverable error' do
115 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
116 | allow(ActiveRecord::Base.connection).to receive(:reconnect!).and_raise(Mysql2::Error.new("fake"))
117 |
118 | expect {
119 | Database.with_reconnect do
120 | @foo.bar
121 | end
122 | }.to raise_error(Mysql2::Error)
123 | end
124 |
125 | it 'does not reconnect if there was an error but the connection is active' do
126 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
127 |
128 | expect {
129 | Database.with_reconnect do
130 | @foo.bar
131 | end
132 | }.to raise_error(ActiveRecord::ActiveRecordError)
133 |
134 | expect(@foo).to have_received(:bar)
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/spec/models/plan_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Plan do
4 | describe '.build' do
5 | it 'sets the attributes correctly' do
6 | plan = Plan.build(
7 | 'id' => 'plan_id',
8 | 'name' => 'plan_name',
9 | 'description' => 'plan_description',
10 | 'metadata' => { 'meta_key' => 'meta_value' },
11 | 'max_storage_mb' => 50,
12 | 'max_user_connections' => 5,
13 | 'free' => false,
14 | )
15 |
16 | expect(plan.id).to eq('plan_id')
17 | expect(plan.name).to eq('plan_name')
18 | expect(plan.description).to eq('plan_description')
19 | expect(plan.metadata).to eq({ 'meta_key' => 'meta_value' })
20 | expect(plan.max_storage_mb).to eq(50)
21 | expect(plan.max_user_connections).to eq(5)
22 | expect(plan.free).to be false
23 | end
24 |
25 | context 'when the id key is missing' do
26 | it 'raises an error' do
27 | expect {
28 | Plan.build(
29 | 'name' => 'plan_name',
30 | 'description' => 'plan_description',
31 | 'metadata' => { 'meta_key' => 'meta_value' },
32 | 'max_storage_mb' => 50,
33 | 'max_user_connections' => 5,
34 | 'free' => false,
35 | )
36 | }.to raise_error(KeyError, 'key not found: "id"')
37 | end
38 | end
39 |
40 | context 'when the name key is missing' do
41 | it 'raises an error' do
42 | expect {
43 | Plan.build(
44 | 'id' => 'plan_id',
45 | 'description' => 'plan_description',
46 | 'metadata' => { 'meta_key' => 'meta_value' },
47 | 'max_storage_mb' => 50,
48 | 'max_user_connections' => 5,
49 | 'free' => false,
50 | )
51 | }.to raise_error(KeyError, 'key not found: "name"')
52 | end
53 | end
54 |
55 | context 'when the description key is missing' do
56 | it 'raises an error' do
57 | expect {
58 | Plan.build(
59 | 'id' => 'plan_id',
60 | 'name' => 'plan_name',
61 | 'metadata' => { 'meta_key' => 'meta_value' },
62 | 'max_storage_mb' => 50,
63 | 'max_user_connections' => 5,
64 | 'free' => false,
65 | )
66 | }.to raise_error(KeyError, 'key not found: "description"')
67 | end
68 | end
69 |
70 | context 'when the metadata key is missing' do
71 | let(:plan) do
72 | Plan.build(
73 | 'id' => 'plan_id',
74 | 'name' => 'plan_name',
75 | 'description' => 'plan_description',
76 | 'max_storage_mb' => 50,
77 | 'max_user_connections' => 5,
78 | 'free' => false,
79 | )
80 | end
81 |
82 | it 'sets the field to nil' do
83 | expect(plan.metadata).to be_nil
84 | end
85 | end
86 |
87 | context 'when the max_storage_mb key is missing' do
88 | let(:plan) do
89 | Plan.build(
90 | 'id' => 'plan_id',
91 | 'name' => 'plan_name',
92 | 'description' => 'plan_description',
93 | 'metadata' => { 'meta_key' => 'meta_value' },
94 | 'max_user_connections' => 5,
95 | 'free' => false,
96 | )
97 | end
98 |
99 | it 'sets the field to nil' do
100 | expect(plan.max_storage_mb).to be_nil
101 | end
102 | end
103 |
104 | context 'when the max_user_connections key is missing' do
105 | let(:plan) do
106 | Plan.build(
107 | 'id' => 'plan_id',
108 | 'name' => 'plan_name',
109 | 'description' => 'plan_description',
110 | 'metadata' => { 'meta_key' => 'meta_value' },
111 | 'max_storage_mb' => 50,
112 | 'free' => false,
113 | )
114 | end
115 |
116 | it 'sets the field to nil' do
117 | expect(plan.max_user_connections).to be_nil
118 | end
119 | end
120 |
121 | context 'when the free key is missing' do
122 | let(:plan) do
123 | Plan.build(
124 | 'id' => 'plan_id',
125 | 'name' => 'plan_name',
126 | 'description' => 'plan_description',
127 | 'metadata' => { 'meta_key' => 'meta_value' },
128 | 'max_storage_mb' => 50,
129 | 'max_user_connections' => 5,
130 | )
131 | end
132 |
133 | it 'sets the field to true' do
134 | expect(plan.free).to be true
135 | end
136 | end
137 | end
138 |
139 | describe '#to_hash' do
140 | it 'contains the correct values' do
141 | plan = Plan.new(
142 | 'id' => 'plan_id',
143 | 'name' => 'plan_name',
144 | 'description' => 'plan_description',
145 | 'metadata' => { 'key1' => 'value1' },
146 | 'max_storage_mb' => 50,
147 | 'max_user_connections' => 5,
148 | 'free' => false,
149 | )
150 |
151 | expect(plan.to_hash.fetch('id')).to eq('plan_id')
152 | expect(plan.to_hash.fetch('name')).to eq('plan_name')
153 | expect(plan.to_hash.fetch('description')).to eq('plan_description')
154 | expect(plan.to_hash.fetch('metadata')).to eq({ 'key1' => 'value1' })
155 | expect(plan.to_hash.fetch('max_storage_mb')).to eq(50)
156 | expect(plan.to_hash.fetch('max_user_connections')).to eq(5)
157 | expect(plan.to_hash.fetch('free')).to be false
158 | end
159 | end
160 | end
161 |
--------------------------------------------------------------------------------
/spec/models/service_binding_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ServiceBinding do
4 | let(:id) { 'fa790aea-ab7f-41e8-b6f9-a2a1d60403f5' }
5 | let(:username) { Digest::MD5.base64digest(id)[0...16] }
6 | let(:password) { 'randompassword' }
7 | let(:binding) { ServiceBinding.new(id: id, service_instance: instance) }
8 |
9 | let(:instance_guid) { '88f6fa22-c8b7-4cdc-be3a-dc09ea7734db' }
10 | let(:plan_guid) { 'plan-guid' }
11 | let(:instance) { ServiceInstance.new(
12 | guid: instance_guid,
13 | plan_guid: plan_guid,
14 | db_name: database)
15 | }
16 | let(:database) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid) }
17 | let(:connection_quota) { 12 }
18 |
19 | before do
20 | allow(SecureRandom).to receive(:base64).and_return(password, 'notthepassword')
21 | allow(Database).to receive(:exists?).with(database).and_return(true)
22 | allow(Catalog).to receive(:connection_quota_for_plan_guid).with(plan_guid).and_return(connection_quota)
23 | allow(Settings).to receive(:allow_table_locks).and_return(true)
24 | end
25 |
26 | after do
27 | begin
28 | allow(connection).to receive(:execute).and_call_original
29 | connection.execute("DROP USER #{username}")
30 | rescue ActiveRecord::StatementInvalid => e
31 | raise unless e.message =~ /DROP USER failed/
32 | end
33 | end
34 |
35 | describe '.find_by_id' do
36 | context 'when the user exists' do
37 | before { connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'") }
38 |
39 | it 'returns the binding' do
40 | binding = ServiceBinding.find_by_id(id)
41 | expect(binding).to be_a(ServiceBinding)
42 | expect(binding.id).to eq(id)
43 | end
44 | end
45 |
46 | context 'when the user does not exist' do
47 | it 'returns nil' do
48 | binding = ServiceBinding.find_by_id(id)
49 | expect(binding).to be_nil
50 | end
51 | end
52 | end
53 |
54 | describe '.find_by_id_and_service_instance_guid' do
55 | context 'when the user exists and has all privileges' do
56 | before { binding.save }
57 |
58 | it 'returns the binding' do
59 | binding = ServiceBinding.find_by_id_and_service_instance_guid(id, instance_guid)
60 | expect(binding).to be_a(ServiceBinding)
61 | expect(binding.id).to eq(id)
62 | end
63 | end
64 |
65 | context 'when the user exists but does not have all privileges' do
66 | before { connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'") }
67 |
68 | it 'returns nil' do
69 | binding = ServiceBinding.find_by_id_and_service_instance_guid(id, instance_guid)
70 | expect(binding).to be_nil
71 | end
72 | end
73 |
74 | context 'when the user does not exist' do
75 | it 'returns nil' do
76 | binding = ServiceBinding.find_by_id_and_service_instance_guid(id, instance_guid)
77 | expect(binding).to be_nil
78 | end
79 | end
80 | end
81 |
82 | describe '.update_all_max_user_connections' do
83 | let(:users) { ["fake-user"] }
84 | let(:plan) do
85 | Plan.new(
86 | {
87 | 'id' => plan_guid,
88 | 'max_user_connections' => 45,
89 | 'name' => 'fake-plan-name',
90 | 'description' => 'some-silly-description',
91 | }
92 | )
93 | end
94 |
95 | before do
96 | allow(Catalog).to receive(:plans).and_return([plan])
97 | end
98 |
99 | it 'updates max user connections for all plans' do
100 | expect(Catalog).to receive(:plans)
101 | expect(connection).to receive(:select_values).
102 | with(
103 | <<-SQL
104 | SELECT mysql.user.user
105 | FROM service_instances
106 | JOIN mysql.db ON service_instances.db_name=mysql.db.Db
107 | JOIN mysql.user ON mysql.user.User=mysql.db.User
108 | WHERE plan_guid='#{plan.id}' AND mysql.user.user NOT LIKE 'root'
109 | SQL
110 | ).and_return(users)
111 |
112 | expect(connection).to receive(:execute).
113 | with(
114 | <<-SQL
115 | GRANT USAGE ON *.* TO '#{users[0]}'@'%'
116 | WITH MAX_USER_CONNECTIONS #{plan.max_user_connections}
117 | SQL
118 | )
119 | ServiceBinding.update_all_max_user_connections
120 | end
121 | end
122 |
123 | describe '.exists?' do
124 | context 'when the user exists and has all privileges' do
125 | before { binding.save }
126 |
127 | it 'returns true' do
128 | expect(ServiceBinding.exists?(id: id, service_instance_guid: instance_guid)).to eq(true)
129 | end
130 | end
131 |
132 | context 'when the user exists but does not have all privileges' do
133 | before { connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'") }
134 |
135 | it 'returns false' do
136 | expect(ServiceBinding.exists?(id: id, service_instance_guid: instance_guid)).to eq(false)
137 | end
138 | end
139 |
140 | context 'when the user does not exist' do
141 | it 'returns false' do
142 | expect(ServiceBinding.exists?(id: id, service_instance_guid: instance_guid)).to eq(false)
143 | end
144 | end
145 | end
146 |
147 | describe '#username' do
148 | it 'returns the same username for a given id' do
149 | binding1 = ServiceBinding.new(id: 'some_id')
150 | binding2 = ServiceBinding.new(id: 'some_id')
151 | expect(binding1.username).to eq (binding2.username)
152 | end
153 |
154 | it 'returns different usernames for different ids' do
155 | binding1 = ServiceBinding.new(id: 'some_id')
156 | binding2 = ServiceBinding.new(id: 'some_other_id')
157 | expect(binding2.username).to_not eq (binding1.username)
158 | end
159 |
160 | it 'returns only alphanumeric characters' do
161 | # MySQL doesn't explicitly require this, but we're doing it to be safe
162 | binding = ServiceBinding.new(id: '~!@#$%^&*()_+{}|:"<>?')
163 | expect(binding.username).to match /^[a-zA-Z0-9]+$/
164 | end
165 |
166 | it 'returns no more than 16 characters' do
167 | # MySQL usernames cannot be greater than 16 characters
168 | binding = ServiceBinding.new(id: 'fa790aea-ab7f-41e8-b6f9-a2a1d60403f5')
169 | expect(binding.username.length).to be <= 16
170 | end
171 | end
172 |
173 | describe '#save' do
174 | it 'creates a user with a random password' do
175 | expect {
176 | binding.save
177 | }.to change {
178 | password_sql = "SELECT * FROM mysql.user WHERE user = '#{username}' AND password = PASSWORD('#{password}')"
179 | connection.select_values(password_sql).count
180 | }.from(0).to(1)
181 | end
182 |
183 | context 'when table locks are enabled' do
184 | it 'grants the user all privileges including for LOCK TABLES' do
185 | expect {
186 | connection.select_values("SHOW GRANTS FOR #{username}")
187 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/)
188 |
189 | binding.save
190 |
191 | grants = connection.select_values("SHOW GRANTS FOR #{username}")
192 |
193 | matching_grants = grants.select { |grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/) }
194 |
195 | expect(matching_grants.length).to eq(1)
196 | expect(matching_grants[0]).to include("ALL PRIVILEGES")
197 | end
198 | end
199 |
200 | context 'when table locks are disabled' do
201 | before do
202 | allow(Settings).to receive(:allow_table_locks).and_return(false)
203 | end
204 |
205 | it 'grants the user all privileges except for LOCK TABLES' do
206 | expect {
207 | connection.select_values("SHOW GRANTS FOR #{username}")
208 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/)
209 |
210 | binding.save
211 |
212 | grants = connection.select_values("SHOW GRANTS FOR #{username}")
213 |
214 | matching_grants = grants.select { |grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/) }
215 |
216 | expect(matching_grants.length).to eq(1)
217 | expect(matching_grants[0]).not_to include("ALL PRIVILEGES")
218 | expect(matching_grants[0]).not_to include("LOCK TABLES")
219 | end
220 | end
221 |
222 | it 'sets the max connections to the value specified by the plan' do
223 | binding.save
224 |
225 | max_user_connection_sql = "WITH MAX_USER_CONNECTIONS #{connection_quota}"
226 | expect(connection.select_values("SHOW GRANTS FOR #{username}")[0]).to include(max_user_connection_sql)
227 | end
228 |
229 | it 'raises an error when creating the same user twice' do
230 | binding.save
231 |
232 | expect {
233 | ServiceBinding.new(id: id, service_instance: instance).save
234 | }.to raise_error(ActiveRecord::StatementInvalid)
235 |
236 | password_sql = "SELECT * FROM mysql.user WHERE user = '#{username}' AND password = PASSWORD('#{password}')"
237 | expect(connection.select_values(password_sql).count).to eq(1)
238 | end
239 |
240 | context 'when the database does not exist' do
241 | before { allow(Database).to receive(:exists?).with(database).and_return(false) }
242 |
243 | it 'raises an error' do
244 | expect{binding.save}.to raise_error(DatabaseNotFoundError)
245 | end
246 | end
247 |
248 | context 'when an error occurs creating the user' do
249 |
250 | let(:db_error) do
251 | ActiveRecord::StatementInvalid.new(
252 | "Lost connection to MySQL server during query: CREATE USER '#{username}' IDENTIFIED BY '#{password}'")
253 | end
254 |
255 | before do
256 | expect(connection).to receive(:execute).with(/CREATE USER/).and_raise(db_error)
257 | end
258 |
259 | it 'redacts the password before re-raising the error' do
260 | expect{binding.save}.to raise_error { |error|
261 | expect(error.message).to_not include password
262 | }
263 | end
264 |
265 | it 'retains the original error message' do
266 | expect{binding.save}.to raise_error { |error|
267 | expect(error.message).to eq "Lost connection to MySQL server during query: CREATE USER '#{username}' IDENTIFIED BY 'redacted'"
268 | }
269 | end
270 |
271 | it 'retains the original error backtrace' do
272 | expect{binding.save}.to raise_error { |error|
273 | expect(error.backtrace).to eq db_error.backtrace
274 | }
275 | end
276 | end
277 |
278 | context 'when read_only is true' do
279 | before do
280 | binding.read_only = true
281 | end
282 |
283 | it 'creates an entry in the read-only table' do
284 | binding.save
285 |
286 | read_only_user = ReadOnlyUser.find_by_username(username)
287 | expect(read_only_user).to be_present
288 | expect(read_only_user.grantee).to eq("'#{username}'@'%'")
289 | end
290 |
291 | it 'grants the correct set of privileges' do
292 | binding.save
293 |
294 | grants = connection.select_values("SHOW GRANTS FOR #{username}")
295 |
296 | matching_grants = grants.select { |grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/) }
297 |
298 | expect(matching_grants.length).to eq(1)
299 | expect(matching_grants[0]).to include("GRANT SELECT ON")
300 | end
301 | end
302 | end
303 |
304 | describe '#destroy' do
305 | context 'when the user exists' do
306 | before { binding.save }
307 |
308 | it 'deletes the user' do
309 | grant_sql_regex = /GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/
310 | grants = connection.select_values("SHOW GRANTS FOR #{username}")
311 | expect(grants.any? { |grant| grant.match(grant_sql_regex) }).to be_truthy
312 |
313 | binding.destroy
314 |
315 | expect {
316 | connection.select_values("SHOW GRANTS FOR #{username}")
317 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/)
318 | end
319 |
320 | context 'when the user is read-only' do
321 | let(:binding) { ServiceBinding.new(id: id, service_instance: instance, read_only: true) }
322 |
323 | it 'also deletes the ReadOnlyUser record' do
324 | expect(ReadOnlyUser.find_by_username(binding.username)).to be_present
325 |
326 | binding.destroy
327 |
328 | expect(ReadOnlyUser.find_by_username(binding.username)).not_to be_present
329 | end
330 | end
331 | end
332 |
333 | context 'when the user does not exist' do
334 | it 'does not raise an error' do
335 | expect {
336 | connection.select_values("SHOW GRANTS FOR #{username}")
337 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/)
338 |
339 | expect {
340 | binding.destroy
341 | }.to_not raise_error
342 |
343 | expect {
344 | connection.select_values("SHOW GRANTS FOR #{username}")
345 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/)
346 | end
347 | end
348 | end
349 |
350 | describe '#to_json' do
351 | let(:connection_config) { Rails.configuration.database_configuration[Rails.env] }
352 | let(:host) { connection_config.fetch('host') }
353 | let(:port) { connection_config.fetch('port') }
354 | let(:uri) { "mysql://#{username}:#{password}@#{host}:#{port}/#{database}?reconnect=true" }
355 | let(:jdbc_url) { "jdbc:mysql://#{host}:#{port}/#{database}?user=#{username}&password=#{password}" }
356 | let(:tls_ca_certificate) { nil }
357 |
358 | before do
359 | allow(Settings).to receive(:[]).with('tls_ca_certificate').and_return(tls_ca_certificate)
360 | binding.save
361 | end
362 |
363 | it 'includes the credentials' do
364 | hash = JSON.parse(binding.to_json)
365 | credentials = hash.fetch('credentials')
366 | expect(credentials.fetch('hostname')).to eq(host)
367 | expect(credentials.fetch('port')).to eq(port)
368 | expect(credentials.fetch('name')).to eq(database)
369 | expect(credentials.fetch('username')).to eq(username)
370 | expect(credentials.fetch('password')).to eq(password)
371 | expect(credentials.fetch('uri')).to eq(uri)
372 | expect(credentials.fetch('jdbcUrl')).to eq(jdbc_url)
373 | expect(credentials).to_not have_key('ca_certificate')
374 | end
375 |
376 | context 'when the broker starts with a ca cert' do
377 | let(:tls_ca_certificate) { 'this-is-a-ca-certificate' }
378 |
379 | it 'includes the ca_certificate' do
380 | hash = JSON.parse(binding.to_json)
381 | credentials = hash.fetch('credentials')
382 | expect(credentials.fetch('ca_certificate')).to eq(tls_ca_certificate)
383 | end
384 |
385 | it 'adds useSSL to the jdbc url' do
386 | hash = JSON.parse(binding.to_json)
387 | credentials = hash.fetch('credentials')
388 | expect(credentials.fetch('jdbcUrl')).to eq("#{jdbc_url}&useSSL=true&enabledTLSProtocols=TLSv1.2&enabledSslProtocolSuites=TLSv1.2")
389 | end
390 | end
391 | end
392 | end
393 |
--------------------------------------------------------------------------------
/spec/models/service_instance_access_verifier_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ServiceInstanceAccessVerifier do
4 | let(:http_client) { double(CloudControllerHttpClient, get: {}) }
5 | let(:instance_guid) { 'an-instance-guid'}
6 |
7 | describe '#can_manage_instance?' do
8 | it 'makes a request to CC' do
9 | ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client)
10 | expect(http_client).to have_received(:get).with("/v2/service_instances/#{instance_guid}/permissions")
11 | end
12 |
13 | context 'when the user does not approve cloud_controller_service_permissions.read' do
14 | before do
15 | allow(http_client).to receive(:get).and_return(nil)
16 | end
17 |
18 | it 'returns false' do
19 | expect(ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client)).to eql(false)
20 | end
21 | end
22 |
23 | context 'when the user can manage the service instance' do
24 | before do
25 | allow(http_client).to receive(:get).and_return({'manage' => true})
26 | end
27 | it 'returns true' do
28 | expect(ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client)).to eql(true)
29 | end
30 | end
31 |
32 | context "when the user can't manage the service instance" do
33 | before do
34 | allow(http_client).to receive(:get).and_return({'manage' => false})
35 | end
36 | it 'returns false' do
37 | expect(ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client)).to eql(false)
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/models/service_instance_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ServiceInstance do
4 | let(:plan_guid_1) { '8888-ffff' }
5 | let(:max_storage_mb_1) { 8 }
6 | let(:plan_guid_2) { '4444-ffff' }
7 | let(:max_storage_mb_2) { 4 }
8 |
9 | let(:instance_guid_1) { 'instance-guid-1' }
10 | let(:instance_guid_2) { 'instance-guid-2' }
11 |
12 | before do
13 | allow(Catalog).to receive(:has_plan?).with(plan_guid_1).and_return(true)
14 | allow(Catalog).to receive(:has_plan?).with(plan_guid_2).and_return(true)
15 |
16 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_guid_1).and_return(max_storage_mb_1)
17 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_guid_2).and_return(max_storage_mb_2)
18 | end
19 |
20 | describe '.reserved_space_in_mb' do
21 | it 'returns 0 when no instances have been created' do
22 | expect(ServiceInstance.reserved_space_in_mb).to eq 0
23 | end
24 |
25 | it 'returns the sum of max_storage_mb for all existing instances' do
26 | ServiceInstanceManager.create(guid: instance_guid_1, plan_guid: plan_guid_1)
27 | ServiceInstanceManager.create(guid: instance_guid_2, plan_guid: plan_guid_2)
28 |
29 | expect(ServiceInstance.reserved_space_in_mb).to eq 12
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/models/service_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Service do
4 | describe '.build' do
5 | before do
6 | allow(Plan).to receive(:build)
7 | end
8 |
9 | it 'sets fields correctly' do
10 | service = Service.build(
11 | 'id' => 'my-id',
12 | 'name' => 'my-name',
13 | 'description' => 'my description',
14 | 'tags' => ['tagA', 'tagB'],
15 | 'metadata' => { 'stuff' => 'goes here' },
16 | 'plans' => [],
17 | 'dashboard_client' => {
18 | 'id' => 'sso-client',
19 | 'secret' => 'something-secret',
20 | 'redirect_uri' => 'example.com'
21 | },
22 | 'plan_updateable' => true
23 | )
24 | expect(service.id).to eq('my-id')
25 | expect(service.name).to eq('my-name')
26 | expect(service.description).to eq('my description')
27 | expect(service.tags).to eq(['tagA', 'tagB'])
28 | expect(service.metadata).to eq({ 'stuff' => 'goes here' })
29 | expect(service.dashboard_client).to eql({
30 | 'id' => 'sso-client',
31 | 'secret' => 'something-secret',
32 | 'redirect_uri' => 'example.com'
33 | })
34 | expect(service.plan_updateable).to eq true
35 | end
36 |
37 | it 'is bindable' do
38 | service = Service.build(
39 | 'id' => 'my-id',
40 | 'name' => 'my-name',
41 | 'description' => 'my description',
42 | 'tags' => ['tagA', 'tagB'],
43 | 'metadata' => { 'stuff' => 'goes here' },
44 | 'plans' => []
45 | )
46 |
47 | expect(service).to be_bindable
48 | end
49 |
50 | it 'builds plans and sets the plans field' do
51 | plan_attrs = [double(:plan_attr1), double(:plan_attr2)]
52 | plan1 = double(:plan1)
53 | plan2 = double(:plan2)
54 |
55 | allow(Plan).to receive(:build).with(plan_attrs[0]).and_return(plan1)
56 | allow(Plan).to receive(:build).with(plan_attrs[1]).and_return(plan2)
57 |
58 | service = Service.build(
59 | 'plans' => plan_attrs,
60 | 'id' => 'my-id',
61 | 'name' => 'my-name',
62 | 'description' => 'my description',
63 | 'tags' => ['tagA', 'tagB'],
64 | 'metadata' => { 'stuff' => 'goes here' },
65 | )
66 |
67 | expect(service.plans).to eq([plan1, plan2])
68 | end
69 |
70 | context 'when the metadata attr is missing' do
71 | let(:service) do
72 | Service.build(
73 | 'id' => 'my-id',
74 | 'name' => 'my-name',
75 | 'description' => 'my description',
76 | 'tags' => ['tagA', 'tagB'],
77 | 'plans' => []
78 | )
79 | end
80 |
81 | it 'sets the field to nil' do
82 | expect(service.metadata).to be_nil
83 | end
84 | end
85 |
86 | context 'when the tags attr is missing' do
87 | let(:service) do
88 | Service.build(
89 | 'id' => 'my-id',
90 | 'name' => 'my-name',
91 | 'description' => 'my description',
92 | 'metadata' => { 'stuff' => 'goes here' },
93 | 'plans' => []
94 | )
95 | end
96 |
97 | it 'sets the field to an empty array' do
98 | expect(service.tags).to eq([])
99 | end
100 | end
101 |
102 | context 'when the dashboard_client attr is missing' do
103 | let(:service) do
104 | Service.build(
105 | 'id' => 'my-id',
106 | 'name' => 'my-name',
107 | 'description' => 'my description',
108 | 'metadata' => { 'stuff' => 'goes here' },
109 | 'plans' => []
110 | )
111 | end
112 |
113 | it 'sets the field to an empty hash' do
114 | expect(service.dashboard_client).to eql({})
115 | end
116 | end
117 |
118 | context 'when the plan_updateable attr is missing' do
119 | let(:service) do
120 | Service.build(
121 | 'id' => 'my-id',
122 | 'name' => 'my-name',
123 | 'description' => 'my description',
124 | 'metadata' => { 'stuff' => 'goes here' },
125 | 'plans' => [],
126 | )
127 | end
128 |
129 | it 'sets the field to false' do
130 | expect(service.plan_updateable).to eql(false)
131 | end
132 | end
133 | end
134 |
135 | describe '#to_hash' do
136 | it 'contains the right values' do
137 | service = Service.new(
138 | 'id' => 'my-id',
139 | 'name' => 'my-name',
140 | 'description' => 'my-description',
141 | 'tags' => ['tagA', 'tagB'],
142 | 'metadata' => { 'meta' => 'data' },
143 | 'plans' => [],
144 | 'dashboard_client' => {
145 | 'id' => 'sso-client',
146 | 'secret' => 'something-secret',
147 | 'redirect_uri' => 'example.com'
148 | },
149 | 'plan_updateable' => true
150 | )
151 |
152 | expect(service.to_hash.fetch('id')).to eq('my-id')
153 | expect(service.to_hash.fetch('name')).to eq('my-name')
154 | expect(service.to_hash.fetch('bindable')).to eq(true)
155 | expect(service.to_hash.fetch('description')).to eq('my-description')
156 | expect(service.to_hash.fetch('tags')).to eq(['tagA', 'tagB'])
157 | expect(service.to_hash.fetch('metadata')).to eq({ 'meta' => 'data' })
158 | expect(service.to_hash).to have_key('plans')
159 | expect(service.to_hash.fetch('dashboard_client')).to eq({
160 | 'id' => 'sso-client',
161 | 'secret' => 'something-secret',
162 | 'redirect_uri' => 'example.com'
163 | })
164 | expect(service.to_hash.fetch('plan_updateable')).to eq true
165 | end
166 |
167 | it 'includes the #to_hash for each plan' do
168 | plan_1 = double(:plan_1)
169 | plan_2 = double(:plan_2)
170 | plan_1_to_hash = double(:plan_1_to_hash)
171 | plan_2_to_hash = double(:plan_2_to_hash)
172 |
173 | expect(plan_1).to receive(:to_hash).and_return(plan_1_to_hash)
174 | expect(plan_2).to receive(:to_hash).and_return(plan_2_to_hash)
175 |
176 | service = Service.new(
177 | 'plans' => [plan_1, plan_2],
178 | 'id' => 'my-id',
179 | 'name' => 'my-name',
180 | 'description' => 'my-description',
181 | 'tags' => ['tagA', 'tagB'],
182 | 'metadata' => { 'meta' => 'data' },
183 | )
184 |
185 | expect(service.to_hash.fetch('plans')).to eq([plan_1_to_hash, plan_2_to_hash])
186 | end
187 |
188 | context 'when there is no plans key' do
189 | let(:service) do
190 | Service.build(
191 | 'id' => 'my-id',
192 | 'name' => 'my-name',
193 | 'description' => 'my-description',
194 | 'tags' => ['tagA', 'tagB'],
195 | 'metadata' => { 'meta' => 'data' },
196 | )
197 | end
198 |
199 | it 'has an empty list of plans' do
200 | expect(service.to_hash.fetch('plans')).to eq([])
201 | end
202 | end
203 |
204 | # There might be a dangling "plans:" in the yaml, which assigns a nil value
205 | context 'when the plans key is nil' do
206 | let(:service) do
207 | Service.build(
208 | 'id' => 'my-id',
209 | 'name' => 'my-name',
210 | 'description' => 'my-description',
211 | 'tags' => ['tagA', 'tagB'],
212 | 'metadata' => { 'meta' => 'data' },
213 | 'plans' => nil,
214 | )
215 | end
216 |
217 | it 'has an empty list of plans' do
218 | expect(service.to_hash.fetch('plans')).to eq([])
219 | end
220 | end
221 | end
222 | end
223 |
--------------------------------------------------------------------------------
/spec/queries/service_instance_usage_query_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ServiceInstanceUsageQuery do
4 | describe 'getting MB used' do
5 | let(:instance_guid) { SecureRandom.uuid }
6 | let(:instance) { ServiceInstance.find_by_guid(instance_guid) }
7 | let(:binding_id) { SecureRandom.uuid }
8 | let(:binding) { ServiceBinding.new(id: binding_id, service_instance: instance) }
9 | let(:mbs_used) { 10 }
10 | let(:client) { create_mysql_client }
11 | let(:max_storage_mb) { 15 }
12 | let(:plan_guid) {'plan_guid'}
13 |
14 | before do
15 | allow(Catalog).to receive(:has_plan?).with(plan_guid).and_return(true)
16 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_guid).and_return(max_storage_mb)
17 | ServiceInstanceManager.create(guid: instance_guid, plan_guid: plan_guid)
18 | allow(Settings).to receive(:allow_table_locks).and_return(true)
19 | binding.save
20 | fill_db
21 | end
22 |
23 | after do
24 | binding.destroy
25 | ServiceInstanceManager.destroy(guid: instance_guid)
26 | end
27 |
28 | it 'returns the correct MB used' do
29 | query = ServiceInstanceUsageQuery.new(instance)
30 |
31 | result = query.execute
32 |
33 | expect(result).to be_within(1).of(mbs_used)
34 | end
35 | end
36 |
37 | def fill_db
38 | data = '1' * (1024 * 1024) # 1 MB
39 | data = client.escape(data)
40 |
41 | client.query('CREATE TABLE stuff (id INT PRIMARY KEY, data LONGTEXT) ENGINE=InnoDB')
42 | mbs_used.times do |n|
43 | client.query("INSERT INTO stuff (id, data) VALUES (#{n}, '#{data}')")
44 | end
45 | end
46 |
47 | def create_mysql_client
48 | Mysql2::Client.new(
49 | :host => binding.host,
50 | :port => binding.port,
51 | :database => binding.database_name,
52 | :username => binding.username,
53 | :password => binding.password
54 | )
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/requests/catalog_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'GET /v2/catalog' do
4 | it 'returns the catalog of services' do
5 | get '/v2/catalog'
6 |
7 | expect(response.status).to eq(200)
8 | catalog = JSON.parse(response.body)
9 | service_settings = YAML.load_file(Rails.root + 'config/settings.yml').fetch('test').fetch('services').first
10 |
11 | services = catalog.fetch('services')
12 | expect(services.size).to eq 1
13 |
14 | service = services.first
15 | expect(service.fetch('name')).to eq(service_settings.fetch('name'))
16 | expect(service.fetch('description')).to eq(service_settings.fetch('description'))
17 | expect(service.fetch('bindable')).to be true
18 | expect(service.fetch('metadata')).to eq(service_settings.fetch('metadata'))
19 |
20 | plans = service.fetch('plans')
21 | expect(plans.size).to eq 2
22 |
23 | plan = plans.first
24 | plan_settings = service_settings.fetch('plans').first
25 | expect(plan.fetch('name')).to eq(plan_settings.fetch('name'))
26 | expect(plan.fetch('description')).to eq(plan_settings.fetch('description'))
27 | expect(plan.fetch('metadata')).to eq(plan_settings.fetch('metadata'))
28 |
29 | plan = plans[1]
30 | plan_settings = service_settings.fetch('plans')[1]
31 | expect(plan.fetch('name')).to eq(plan_settings.fetch('name'))
32 | expect(plan.fetch('description')).to eq(plan_settings.fetch('description'))
33 | expect(plan.fetch('metadata')).to eq(plan_settings.fetch('metadata'))
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/requests/lifecycle_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | # Provisions, binds, unbinds, deprovisions a service
4 |
5 | def cleanup_mysql_user(username)
6 | ActiveRecord::Base.connection.execute("DROP USER #{username}")
7 | rescue
8 | end
9 |
10 | def cleanup_mysql_database(dbname)
11 | ActiveRecord::Base.connection.execute("DROP DATABASE #{dbname}")
12 | rescue
13 | end
14 |
15 | def create_mysql_client(username=Rails.configuration.database_configuration[Rails.env].fetch('username'),
16 | password=Rails.configuration.database_configuration[Rails.env].fetch('password'),
17 | database='mysql')
18 | Mysql2::Client.new(
19 | :host => Rails.configuration.database_configuration[Rails.env].fetch('host'),
20 | :port => Rails.configuration.database_configuration[Rails.env].fetch('port'),
21 | :database => database,
22 | :username => username,
23 | :password => password
24 | )
25 | end
26 |
27 | describe 'the service lifecycle' do
28 | let(:instance_id) {'instance-1'}
29 | let(:dbname) {'cf_instance_1'}
30 |
31 | let(:binding_id) {'binding-1'}
32 | let(:password) {'somepassword'}
33 | let(:username) {ServiceBinding.new(id: binding_id).username}
34 | let(:headers) {{"CONTENT_TYPE" => "application/json"}}
35 |
36 | before do
37 | cleanup_mysql_user(username)
38 | cleanup_mysql_database(dbname)
39 | allow(Settings).to receive(:allow_table_locks).and_return(true)
40 | end
41 |
42 | after do
43 | cleanup_mysql_user(username)
44 | cleanup_mysql_database(dbname)
45 | end
46 |
47 | describe 'bindings' do
48 | before do
49 | allow(SecureRandom).to receive(:base64).and_return(password, 'notthepassword')
50 |
51 | ##
52 | ## Provision the instance
53 | ##
54 | put "/v2/service_instances/#{instance_id}", {plan_id: '2451fa22-df16-4c10-ba6e-1f682d3dcdc9'}
55 |
56 | expect(response.status).to eq(201)
57 | expect(response.body).to eq("{\"dashboard_url\":\"http://p-mysql.bosh-lite.com/manage/instances/#{instance_id}\"}")
58 | end
59 |
60 | after do
61 | ##
62 | ## Unbind
63 | ##
64 | delete "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}"
65 | expect(response.status).to eq(200)
66 | expect(response.body).to eq('{}')
67 |
68 | ##
69 | ## Test that the binding no longer works
70 | ##
71 | expect {
72 | create_mysql_client(username, password, dbname)
73 | }.to raise_error(Mysql2::Error)
74 |
75 | ##
76 | ## Test that we have purged any data associated with the user
77 | ##
78 | client = create_mysql_client()
79 | found = client.query("SELECT * FROM mysql.db WHERE User = '#{username}';")
80 | expect(found.count).to eq(0)
81 | found = client.query("SELECT * FROM mysql.user WHERE User = '#{username}';")
82 | expect(found.count).to eq(0)
83 |
84 | ##
85 | ## Deprovision
86 | ##
87 | delete "/v2/service_instances/#{instance_id}"
88 | expect(response.status).to eq(200)
89 | expect(response.body).to eq('{}')
90 |
91 | ##
92 | ## Test that the database no longer exists
93 | ##
94 | found = client.query("SHOW DATABASES LIKE '#{dbname}'")
95 | expect(found.count).to eq(0)
96 | end
97 |
98 | describe 'read/write binding' do
99 | it 'provisions, binds, unbinds, deprovisions' do
100 | ##
101 | ## Bind with incorrect arbitrary parameters
102 | ##
103 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}",
104 | JSON.dump({"parameters" => {"read-oooonly" => true}}),
105 | headers.merge(env)
106 |
107 | expect(response.status).to eq(400)
108 | expect(JSON.parse(response.body)).to eq({
109 | "error" => "Error creating service binding",
110 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.",
111 | })
112 |
113 | ##
114 | ## Bind with invalid value for read-only parameter
115 | ##
116 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}",
117 | JSON.dump({"parameters" => {"read-only" => "tomato"}}),
118 | headers.merge(env)
119 |
120 | expect(response.status).to eq(400)
121 | expect(JSON.parse(response.body)).to eq({
122 | "error" => "Error creating service binding",
123 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.",
124 | })
125 |
126 | ##
127 | ## Bind
128 | ##
129 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}"
130 |
131 | expect(response.status).to eq(201)
132 | instance = JSON.parse(response.body)
133 |
134 | expect(instance.fetch('credentials')).to eq({
135 | 'hostname' => 'localhost',
136 | 'name' => dbname,
137 | 'username' => username,
138 | 'password' => password,
139 | 'port' => 3306,
140 | 'jdbcUrl' => "jdbc:mysql://localhost:3306/#{dbname}?user=#{username}&password=#{password}",
141 | 'uri' => "mysql://#{username}:#{password}@localhost:3306/#{dbname}?reconnect=true",
142 | })
143 |
144 | ##
145 | ## Test the binding
146 | ##
147 | client = create_mysql_client(username, password, dbname)
148 |
149 | client.query("CREATE TABLE IF NOT EXISTS data_values (id VARCHAR(20), data_value VARCHAR(20));")
150 | client.query("INSERT INTO data_values VALUES('123', '456');")
151 | found = client.query("SELECT id, data_value FROM data_values;").first
152 | expect(found.fetch('data_value')).to eq('456')
153 | end
154 | end
155 |
156 | describe 'read-only bindings' do
157 | it 'provisions, binds, unbinds, deprovisions' do
158 | ##
159 | ## Make a read-only binding
160 | ##
161 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}",
162 | JSON.dump({"parameters" => {"read-only" => true}}),
163 | headers.merge(env)
164 |
165 | expect(response.status).to eq(201)
166 | instance = JSON.parse(response.body)
167 |
168 | expect(instance.fetch('credentials')).to eq({
169 | 'hostname' => 'localhost',
170 | 'name' => dbname,
171 | 'username' => username,
172 | 'password' => password,
173 | 'port' => 3306,
174 | 'jdbcUrl' => "jdbc:mysql://localhost:3306/#{dbname}?user=#{username}&password=#{password}",
175 | 'uri' => "mysql://#{username}:#{password}@localhost:3306/#{dbname}?reconnect=true",
176 | })
177 |
178 | ##
179 | ## Verify that the binding has read access
180 | ##
181 | client = create_mysql_client(username, password, dbname)
182 | found = client.query("SHOW DATABASES LIKE '#{dbname}'")
183 | expect(found.count).to eq(1)
184 |
185 | ##
186 | ## Verify that the binding does not have write access
187 | ##
188 | expect {
189 | client.query("CREATE TABLE IF NOT EXISTS data_values (id VARCHAR(20), data_value VARCHAR(20));")
190 | }.to raise_error(Mysql2::Error)
191 | end
192 | end
193 | end
194 | end
195 |
--------------------------------------------------------------------------------
/spec/requests/missing_things_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | # Behavior when calling endpoints associated with things that do not exist.
4 |
5 | describe 'endpoints' do
6 | describe 'deleting an instance' do
7 | context 'when the service instance does not exist' do
8 | it 'returns 410' do
9 | delete '/v2/service_instances/DOESNOTEXIST'
10 | expect(response.status).to eq(410)
11 | end
12 | end
13 | end
14 |
15 | describe 'deleting a service binding' do
16 | context 'when the service binding does not exist' do
17 | it 'returns 410' do
18 | delete '/v2/service_instances/service_instance_id/service_bindings/DOESNOTEXIST'
19 | expect(response.status).to eq(410)
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.raise_errors_for_deprecations!
3 | end
4 |
5 | require 'codeclimate-test-reporter'
6 | CodeClimate::TestReporter.start
7 |
8 | # This file is copied to spec/ when you run 'rails generate rspec:install'
9 | ENV['RAILS_ENV'] ||= 'test'
10 | require File.expand_path('../../config/environment', __FILE__)
11 | require 'rspec/rails'
12 | require 'webmock/rspec'
13 | require 'database_cleaner'
14 |
15 | DatabaseCleaner.strategy = :truncation
16 | WebMock.disable_net_connect!(allow: 'codeclimate.com')
17 |
18 | # Requires supporting ruby files with custom matchers and macros, etc,
19 | # in spec/support/ and its subdirectories.
20 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
21 |
22 | # Checks for pending migrations before tests are run.
23 | # If you are not using ActiveRecord, you can remove this line.
24 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)
25 |
26 | RSpec.configure do |config|
27 | # ## Mock Framework
28 | #
29 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
30 | #
31 | # config.mock_with :mocha
32 | # config.mock_with :flexmock
33 | # config.mock_with :rr
34 |
35 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
36 | # config.fixture_path = "#{::Rails.root}/spec/fixtures"
37 |
38 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
39 | # examples within a transaction, remove the following line or assign false
40 | # instead of true.
41 | # config.use_transactional_fixtures = true
42 |
43 | # If true, the base class of anonymous controllers will be inferred
44 | # automatically. This will be the default behavior in future versions of
45 | # rspec-rails.
46 | config.infer_base_class_for_anonymous_controllers = true
47 |
48 | config.infer_spec_type_from_file_location!
49 |
50 | # Run specs in random order to surface order dependencies. If you find an
51 | # order dependency and want to debug it, you can fix the order by providing
52 | # the seed, which is printed after each run.
53 | # --seed 1234
54 | config.order = 'random'
55 |
56 | #
57 | # Our tests won't work with the default localhost wildcard user record in mysql
58 | # We also need a few innodb settings in order for stats to update automatically.
59 | #
60 | config.before :suite do
61 | count_of_bad_users = ActiveRecord::Base.connection.select_value("select count(*) from mysql.user where Host='localhost' and User=''")
62 | if count_of_bad_users > 0
63 | raise %Q{You must delete the Host='localhost' User='' record from the mysql.users table.\nRun this command:\nmysql -u root -e "DELETE FROM mysql.user WHERE Host='localhost' AND User=''"}
64 | end
65 |
66 | variable_records = ActiveRecord::Base.connection.select_rows("show variables like 'innodb_stats_%'")
67 | variables = Hash[
68 | ActiveRecord::Base.connection.select_rows("show variables like 'innodb_stats_%'")
69 | ]
70 |
71 | raise <<-TEXT.strip_heredoc unless variables['innodb_stats_on_metadata'] == 'ON'
72 | innodb_stats_on_metadata must be ON
73 | Option 1 (permanent): set innodb_stats_on_metadata=ON in my.cnf
74 | Option 2 (temporary): `mysql -uroot -e "SET GLOBAL innodb_stats_on_metadata=ON"`
75 | TEXT
76 | raise <<-TEXT.strip_heredoc if variables['innodb_stats_persistent'] == 'ON'
77 | innodb_stats_persistent must be OFF
78 | Option 1 (permanent): set innodb_stats_persistent=OFF in my.cnf
79 | Option 2 (temporary): `mysql -uroot -e "SET GLOBAL innodb_stats_persistent=OFF"`
80 | TEXT
81 | end
82 |
83 | config.after do
84 | DatabaseCleaner.clean
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/support/controller_helpers.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for 'a controller action that does not log its request and response headers and body' do
2 | it 'does not log the request' do
3 | expect(Rails.logger).to_not receive(:info).with(/Request:/)
4 | make_request
5 | end
6 |
7 | it 'does not log the response' do
8 | expect(Rails.logger).to_not receive(:info).with(/Response:/)
9 | make_request
10 | end
11 | end
12 |
13 | shared_examples_for 'a controller action that requires basic auth' do
14 | context 'when the basic-auth username is incorrect' do
15 | before do
16 | set_basic_auth('wrong_username', Settings.auth_password)
17 | end
18 |
19 | it 'responds with a 401' do
20 | make_request
21 |
22 | expect(response.status).to eq(401)
23 | end
24 | end
25 | end
26 |
27 | module ControllerHelpers
28 | extend ActiveSupport::Concern
29 |
30 | def authenticate
31 | set_basic_auth(Settings.auth_username, Settings.auth_password)
32 | end
33 |
34 | def set_basic_auth(username, password)
35 | request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
36 | end
37 |
38 | def db
39 | ActiveRecord::Base.connection
40 | end
41 | end
42 |
43 | RSpec.configure do |config|
44 | config.include ControllerHelpers, type: :controller
45 | config.infer_spec_type_from_file_location!
46 | end
47 |
--------------------------------------------------------------------------------
/spec/support/feature_helpers.rb:
--------------------------------------------------------------------------------
1 | # Inspired by rspec-rails' request example group.
2 | module FeatureHelpers
3 | extend ActiveSupport::Concern
4 | include ActionController::TemplateAssertions
5 | include ActionDispatch::Integration::Runner
6 |
7 | included do
8 | metadata[:type] = :feature
9 |
10 | let(:default_env) do
11 | username = Settings.auth_username
12 | password = Settings.auth_password
13 |
14 | {
15 | 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
16 | }
17 | end
18 | let(:env) { default_env }
19 |
20 | before do
21 | @routes = ::Rails.application.routes
22 | end
23 | end
24 |
25 | def app
26 | ::Rails.application
27 | end
28 |
29 | def get(*args)
30 | args[2] ||= env
31 | super(*args)
32 | end
33 |
34 | def post(*args)
35 | args[2] ||= env
36 | super(*args)
37 | end
38 |
39 | def put(*args)
40 | args[2] ||= env
41 | super(*args)
42 | end
43 |
44 | def patch(*args)
45 | args[2] ||= env
46 | super(*args)
47 | end
48 |
49 | def delete(*args)
50 | args[2] ||= env
51 | super(*args)
52 | end
53 |
54 | def create_mysql_client(config)
55 | Mysql2::Client.new(
56 | :host => config.fetch('hostname'),
57 | :port => config.fetch('port'),
58 | :database => config.fetch('name'),
59 | :username => config.fetch('username'),
60 | :password => config.fetch('password')
61 | )
62 | end
63 |
64 | def create_table_and_write_data(client, max_storage_mb)
65 | client.query('DROP TABLE IF EXISTS stuff')
66 | client.query('CREATE TABLE stuff (id INT PRIMARY KEY, data LONGTEXT) ENGINE=InnoDB')
67 |
68 | data = '1' * (1024 * 1024) # 1 MB
69 |
70 | max_storage_mb.times do |n|
71 | client.query("INSERT INTO stuff (id, data) VALUES (#{n}, '#{data}')")
72 | end
73 | end
74 |
75 | def verify_client_can_write(client)
76 | client.query("INSERT INTO stuff (id, data) VALUES (99999, 'This should succeed.')")
77 | client.query("UPDATE stuff SET data = 'This should also succeed.' WHERE id = 99999")
78 | end
79 | end
80 |
81 | RSpec.configure do |config|
82 | config.include FeatureHelpers, :file_path => %r(spec/features)
83 | end
84 |
--------------------------------------------------------------------------------
/spec/support/model_helpers.rb:
--------------------------------------------------------------------------------
1 | module ModelHelpers
2 | def connection
3 | ActiveRecord::Base.connection
4 | end
5 | end
6 |
7 | RSpec.configure do |config|
8 | config.include ModelHelpers, type: :model
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/mysql_helper.rb:
--------------------------------------------------------------------------------
1 | module MysqlHelpers
2 | def create_mysql_client(binding)
3 | Mysql2::Client.new(
4 | :host => binding.host,
5 | :port => binding.port,
6 | :database => binding.database_name,
7 | :username => binding.username,
8 | :password => binding.password
9 | )
10 | end
11 |
12 | def create_root_mysql_client
13 | config = Rails.configuration.database_configuration[Rails.env]
14 |
15 | Mysql2::Client.new(
16 | :host => binding1.host,
17 | :port => binding1.port,
18 | :database => binding1.database_name,
19 | :username => config.fetch('username'),
20 | :password => config.fetch('password')
21 | )
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/support/request_helpers.rb:
--------------------------------------------------------------------------------
1 | module RequestHelpers
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | let(:default_env) do
6 | username = Settings.auth_username
7 | password = Settings.auth_password
8 |
9 | {
10 | 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
11 | }
12 | end
13 | let(:env) { default_env }
14 | end
15 |
16 | def get(*args)
17 | args[2] ||= env
18 | super(*args)
19 | end
20 |
21 | def post(*args)
22 | args[2] ||= env
23 | super(*args)
24 | end
25 |
26 | def put(*args)
27 | args[2] ||= env
28 | super(*args)
29 | end
30 |
31 | def delete(*args)
32 | args[2] ||= env
33 | super(*args)
34 | end
35 | end
36 |
37 | RSpec.configure do |config|
38 | config.include RequestHelpers, type: :request
39 | end
40 |
--------------------------------------------------------------------------------