poise-application-python 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.kitchen.travis.yml +9 -0
  4. data/.kitchen.yml +10 -0
  5. data/.travis.yml +20 -0
  6. data/.yardopts +3 -0
  7. data/Berksfile +35 -0
  8. data/CHANGELOG.md +71 -0
  9. data/Gemfile +37 -0
  10. data/LICENSE +201 -0
  11. data/README.md +334 -0
  12. data/Rakefile +17 -0
  13. data/SUPPORTERS.md +81 -0
  14. data/chef/templates/celeryconfig.py.erb +5 -0
  15. data/chef/templates/settings.py.erb +13 -0
  16. data/lib/poise_application_python.rb +23 -0
  17. data/lib/poise_application_python/app_mixin.rb +67 -0
  18. data/lib/poise_application_python/cheftie.rb +17 -0
  19. data/lib/poise_application_python/error.rb +25 -0
  20. data/lib/poise_application_python/resources.rb +26 -0
  21. data/lib/poise_application_python/resources/celery_beat.rb +43 -0
  22. data/lib/poise_application_python/resources/celery_config.rb +109 -0
  23. data/lib/poise_application_python/resources/celery_worker.rb +77 -0
  24. data/lib/poise_application_python/resources/django.rb +355 -0
  25. data/lib/poise_application_python/resources/gunicorn.rb +127 -0
  26. data/lib/poise_application_python/resources/pip_requirements.rb +47 -0
  27. data/lib/poise_application_python/resources/python.rb +57 -0
  28. data/lib/poise_application_python/resources/python_execute.rb +89 -0
  29. data/lib/poise_application_python/resources/python_package.rb +62 -0
  30. data/lib/poise_application_python/resources/virtualenv.rb +75 -0
  31. data/lib/poise_application_python/service_mixin.rb +57 -0
  32. data/lib/poise_application_python/version.rb +19 -0
  33. data/poise-application-python.gemspec +45 -0
  34. data/test/cookbooks/application_python_test/attributes/default.rb +17 -0
  35. data/test/cookbooks/application_python_test/metadata.rb +20 -0
  36. data/test/cookbooks/application_python_test/recipes/default.rb +83 -0
  37. data/test/cookbooks/application_python_test/recipes/django.rb +32 -0
  38. data/test/cookbooks/application_python_test/recipes/flask.rb +25 -0
  39. data/test/gemfiles/chef-12.gemfile +19 -0
  40. data/test/gemfiles/master.gemfile +27 -0
  41. data/test/integration/default/serverspec/default_spec.rb +81 -0
  42. data/test/integration/default/serverspec/django_spec.rb +56 -0
  43. data/test/integration/default/serverspec/flask_spec.rb +39 -0
  44. data/test/spec/app_mixin_spec.rb +69 -0
  45. data/test/spec/resources/celery_config_spec.rb +58 -0
  46. data/test/spec/resources/django_spec.rb +303 -0
  47. data/test/spec/resources/gunicorn_spec.rb +96 -0
  48. data/test/spec/resources/python_execute_spec.rb +46 -0
  49. data/test/spec/resources/python_spec.rb +44 -0
  50. data/test/spec/resources/virtualenv_spec.rb +44 -0
  51. data/test/spec/spec_helper.rb +19 -0
  52. metadata +216 -0
@@ -0,0 +1,58 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'spec_helper'
18
+
19
+ describe PoiseApplicationPython::Resources::CeleryConfig do
20
+ step_into(:application_celery_config)
21
+ before do
22
+ allow(File).to receive(:directory?).and_call_original
23
+ allow(File).to receive(:directory?).with('/test').and_return(true)
24
+ end
25
+
26
+ context 'with defaults' do
27
+ recipe do
28
+ application_celery_config '/test'
29
+ end
30
+ it { is_expected.to deploy_application_celery_config('/test').with(path: '/test/celeryconfig.py') }
31
+ it { is_expected.to render_file('/test/celeryconfig.py').with_content(eq(<<-CELERYCONFIG)) }
32
+ # Generated by Chef for application_celery_config[/test]
33
+
34
+ CELERYCONFIG
35
+ end # /context with defaults
36
+
37
+ context 'with a specific path' do
38
+ recipe do
39
+ application_celery_config '/test/foo.py'
40
+ end
41
+ it { is_expected.to deploy_application_celery_config('/test/foo.py').with(path: '/test/foo.py') }
42
+ end # /context with a specific path
43
+
44
+ context 'with template options' do
45
+ recipe do
46
+ application_celery_config '/test' do
47
+ options do
48
+ broker_url 'amqp://'
49
+ end
50
+ end
51
+ end
52
+ it { is_expected.to render_file('/test/celeryconfig.py').with_content(eq(<<-CELERYCONFIG)) }
53
+ # Generated by Chef for application_celery_config[/test]
54
+
55
+ BROKER_URL = "amqp://"
56
+ CELERYCONFIG
57
+ end # /context with template options
58
+ end
@@ -0,0 +1,303 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'spec_helper'
18
+
19
+ describe PoiseApplicationPython::Resources::Django do
20
+ describe PoiseApplicationPython::Resources::Django::Resource do
21
+ describe '#local_settings' do
22
+ subject { chef_run.application_django('/test').local_settings_content }
23
+
24
+ context 'with defaults' do
25
+ recipe(subject: false) do
26
+ application_django '/test'
27
+ end
28
+ it { is_expected.to eq <<-SETTINGS }
29
+ # Generated by Chef for application_django[/test]
30
+
31
+ DEBUG = False
32
+
33
+ DATABASES = {"default":{}}
34
+ SETTINGS
35
+ end # /context with defaults
36
+
37
+ context 'with a URL' do
38
+ recipe(subject: false) do
39
+ application_django '/test' do
40
+ database 'postgres://myuser@dbhost/myapp'
41
+ end
42
+ end
43
+ it { is_expected.to eq <<-SETTINGS }
44
+ # Generated by Chef for application_django[/test]
45
+
46
+ DEBUG = False
47
+
48
+ DATABASES = {"default":{"URL":"postgres://myuser@dbhost/myapp","ENGINE":"django.db.backends.postgresql_psycopg2","NAME":"myapp","USER":"myuser","HOST":"dbhost"}}
49
+ SETTINGS
50
+ end # /context with a URL
51
+
52
+ context 'with an options block' do
53
+ recipe(subject: false) do
54
+ application_django '/test' do
55
+ database do
56
+ engine 'postgres'
57
+ name 'myapp'
58
+ user 'myuser'
59
+ host 'dbhost'
60
+ end
61
+ end
62
+ end
63
+ it { is_expected.to eq <<-SETTINGS }
64
+ # Generated by Chef for application_django[/test]
65
+
66
+ DEBUG = False
67
+
68
+ DATABASES = {"default":{"ENGINE":"django.db.backends.postgresql_psycopg2","NAME":"myapp","USER":"myuser","HOST":"dbhost"}}
69
+ SETTINGS
70
+ end # /context with an options block
71
+
72
+ context 'with debug mode' do
73
+ recipe(subject: false) do
74
+ application_django '/test' do
75
+ debug true
76
+ end
77
+ end
78
+ it { is_expected.to eq <<-SETTINGS }
79
+ # Generated by Chef for application_django[/test]
80
+
81
+ DEBUG = True
82
+
83
+ DATABASES = {"default":{}}
84
+ SETTINGS
85
+ end # /context with debug mode
86
+
87
+ context 'with a single allowed host' do
88
+ recipe(subject: false) do
89
+ application_django '/test' do
90
+ allowed_hosts 'example.com'
91
+ end
92
+ end
93
+ it { is_expected.to eq <<-SETTINGS }
94
+ # Generated by Chef for application_django[/test]
95
+
96
+ ALLOWED_HOSTS = ["example.com"]
97
+
98
+ DEBUG = False
99
+
100
+ DATABASES = {"default":{}}
101
+ SETTINGS
102
+ end # /context with a single allowed host
103
+
104
+ context 'with multiple allowed hosts' do
105
+ recipe(subject: false) do
106
+ application_django '/test' do
107
+ allowed_hosts %w{example.com www.example.com}
108
+ end
109
+ end
110
+ it { is_expected.to eq <<-SETTINGS }
111
+ # Generated by Chef for application_django[/test]
112
+
113
+ ALLOWED_HOSTS = ["example.com","www.example.com"]
114
+
115
+ DEBUG = False
116
+
117
+ DATABASES = {"default":{}}
118
+ SETTINGS
119
+ end # /context with multiple allowed hosts
120
+
121
+ context 'with a secret key' do
122
+ recipe(subject: false) do
123
+ application_django '/test' do
124
+ secret_key 'swordfish'
125
+ end
126
+ end
127
+ it { is_expected.to eq <<-SETTINGS }
128
+ # Generated by Chef for application_django[/test]
129
+
130
+ DEBUG = False
131
+
132
+ DATABASES = {"default":{}}
133
+
134
+ SECRET_KEY = "swordfish"
135
+ SETTINGS
136
+ end # /context with a secret key
137
+ end # /describe #local_settings
138
+
139
+ describe '#default_local_settings_path' do
140
+ subject { chef_run.application_django('/test').send(:default_local_settings_path) }
141
+
142
+ context 'with no settings.py' do
143
+ recipe(subject: false) do
144
+ application_django '/test' do
145
+ def settings_module
146
+ nil
147
+ end
148
+ end
149
+ end
150
+ it { is_expected.to be_nil }
151
+ end # /context with no settings.py
152
+
153
+ context 'with basic settings.py' do
154
+ recipe(subject: false) do
155
+ application_django '/test' do
156
+ settings_module 'myapp.settings'
157
+ end
158
+ end
159
+ it { is_expected.to eq '/test/myapp/local_settings.py' }
160
+ end # /context with basic settings.py
161
+ end # /describe #default_local_settings_path
162
+
163
+ describe '#default_manage_path' do
164
+ subject { chef_run.application_django('/test').send(:default_manage_path) }
165
+ recipe(subject: false) do
166
+ application_django '/test'
167
+ end
168
+ before do
169
+ allow(chef_run.application_django('/test')).to receive(:find_file).with('manage.py').and_return('/test/manage.py')
170
+ end
171
+
172
+ it { is_expected.to eq '/test/manage.py' }
173
+ end # /describe #default_manage_path
174
+
175
+ describe '#default_settings_module' do
176
+ let(:settings_path) { nil }
177
+ subject { chef_run.application_django('/test').send(:default_settings_module) }
178
+ recipe(subject: false) do
179
+ application_django '/test'
180
+ end
181
+ before do
182
+ allow(chef_run.application_django('/test')).to receive(:find_file).with('settings.py').and_return(settings_path)
183
+ end
184
+
185
+ context 'with no settings.py' do
186
+ it { is_expected.to be_nil }
187
+ end # /context with no settings.py
188
+
189
+ context 'with simple settings.py' do
190
+ let(:settings_path) { '/test/myapp/settings.py' }
191
+ it { is_expected.to eq 'myapp.settings' }
192
+ end # /context with simple settings.py
193
+ end # /describe #default_settings_module
194
+
195
+ describe '#default_wsgi_module' do
196
+ let(:wsgi_path) { nil }
197
+ subject { chef_run.application_django('/test').send(:default_wsgi_module) }
198
+ recipe(subject: false) do
199
+ application_django '/test'
200
+ end
201
+ before do
202
+ allow(chef_run.application_django('/test')).to receive(:find_file).with('wsgi.py').and_return(wsgi_path)
203
+ end
204
+
205
+ context 'with no wsgi.py' do
206
+ it { is_expected.to be_nil }
207
+ end # /context with no wsgi.py
208
+
209
+ context 'with simple wsgi.py' do
210
+ let(:wsgi_path) { '/test/wsgi.py' }
211
+ it { is_expected.to eq 'wsgi' }
212
+ end # /context with simple wsgi.py
213
+ end # /describe #default_wsgi_module
214
+
215
+ describe '#find_file' do
216
+ let(:files) { [] }
217
+ recipe(subject: false) do
218
+ application_django '/test'
219
+ end
220
+ subject { chef_run.application_django('/test').send(:find_file, 'myfile.py') }
221
+ before do
222
+ allow(Dir).to receive(:[]).and_call_original
223
+ allow(Dir).to receive(:[]).with('/test/**/myfile.py').and_return(files)
224
+ end
225
+
226
+ context 'with no matching files' do
227
+ it { is_expected.to be_nil }
228
+ end # /context with no matching files
229
+
230
+ context 'with one matching file' do
231
+ let(:files) { %w{/test/myfile.py} }
232
+ it { is_expected.to eq '/test/myfile.py' }
233
+ end # /context with one matching file
234
+
235
+ context 'with two matching files' do
236
+ let(:files) { %w{/test/myfile.py /test/sub/myfile.py} }
237
+ it { is_expected.to eq '/test/myfile.py' }
238
+ end # /context with two matching files
239
+
240
+ context 'with two matching files in a different order' do
241
+ let(:files) { %w{/test/sub/myfile.py /test/myfile.py} }
242
+ it { is_expected.to eq '/test/myfile.py' }
243
+ end # /context with two matching files in a different order
244
+
245
+ context 'with two matching files on the same level' do
246
+ let(:files) { %w{/test/b/myfile.py /test/a/myfile.py} }
247
+ it { is_expected.to eq '/test/a/myfile.py' }
248
+ end # /context with two matching files on the same level
249
+ end # /describe #find_file
250
+ end # /describe PoiseApplicationPython::Resources::Django::Resource
251
+
252
+ describe PoiseApplicationPython::Resources::Django::Provider do
253
+ step_into(:application_django)
254
+ context 'with default settings' do
255
+ recipe do
256
+ application_django '/test' do
257
+ # Hardwire all paths so it doesn't have to search.
258
+ manage_path 'manage.py'
259
+ settings_module 'myapp.settings'
260
+ wsgi_module 'wsgi'
261
+ end
262
+ end
263
+
264
+ it { is_expected.to run_python_execute('manage.py collectstatic --noinput') }
265
+ it { is_expected.to_not run_python_execute('manage.py syncdb --noinput') }
266
+ it { is_expected.to_not run_python_execute('manage.py migrate --noinput') }
267
+ it { is_expected.to render_file('/test/myapp/local_settings.py') }
268
+ end # /context with default settings
269
+
270
+ context 'with syncdb' do
271
+ recipe do
272
+ application_django '/test' do
273
+ # Hardwire all paths so it doesn't have to search.
274
+ manage_path 'manage.py'
275
+ settings_module 'myapp.settings'
276
+ syncdb true
277
+ wsgi_module 'wsgi'
278
+ end
279
+ end
280
+
281
+ it { is_expected.to run_python_execute('manage.py collectstatic --noinput') }
282
+ it { is_expected.to run_python_execute('manage.py syncdb --noinput') }
283
+ it { is_expected.to_not run_python_execute('manage.py migrate --noinput') }
284
+ end # /context with syncdb
285
+
286
+ context 'with migrate' do
287
+ recipe do
288
+ application_django '/test' do
289
+ # Hardwire all paths so it doesn't have to search.
290
+ manage_path 'manage.py'
291
+ migrate true
292
+ settings_module 'myapp.settings'
293
+ wsgi_module 'wsgi'
294
+ end
295
+ end
296
+
297
+ it { is_expected.to run_python_execute('manage.py collectstatic --noinput') }
298
+ it { is_expected.to_not run_python_execute('manage.py syncdb --noinput') }
299
+ it { is_expected.to run_python_execute('manage.py migrate --noinput') }
300
+ it { is_expected.to render_file('/test/myapp/local_settings.py') }
301
+ end # /context with migrate
302
+ end # /describe PoiseApplicationPython::Resources::Django::Provider
303
+ end
@@ -0,0 +1,96 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'spec_helper'
18
+
19
+ describe PoiseApplicationPython::Resources::Gunicorn do
20
+ describe PoiseApplicationPython::Resources::Gunicorn::Resource do
21
+ describe '#default_app_module' do
22
+ let(:app_state) { {} }
23
+ let(:files) { [] }
24
+ let(:test_resource) { described_class.new(nil, nil) }
25
+ before do
26
+ allow(test_resource).to receive(:app_state).and_return(app_state)
27
+ allow(Dir).to receive(:exist?).and_return(!files.empty?)
28
+ allow(Dir).to receive(:entries).and_return(files)
29
+ end
30
+ subject { test_resource.send(:default_app_module) }
31
+
32
+ context 'with an app_state key' do
33
+ let(:app_state) { {python_wsgi_module: 'django'} }
34
+ it { is_expected.to eq 'django' }
35
+ end # /context with an app_state key
36
+
37
+ context 'with a wsgi.py' do
38
+ let(:files) { %w{wsgi.py} }
39
+ it { is_expected.to eq 'wsgi' }
40
+ end # /context with a wsgi.py
41
+
42
+ context 'with an app.py and main.py' do
43
+ let(:files) { %w{app.py main.py} }
44
+ it { is_expected.to eq 'main' }
45
+ end # /context with an app.py and main.py
46
+
47
+ context 'with a foo.txt and bar.py' do
48
+ let(:files) { %w{foo.txt bar.py} }
49
+ it { is_expected.to eq 'bar' }
50
+ end # /context with a foo.txt and bar.py
51
+
52
+ context 'with a foo.txt' do
53
+ let(:files) { %w{foo.txt } }
54
+ it { is_expected.to be_nil }
55
+ end # /context with a foo.txt
56
+ end # /describe #default_app_module
57
+ end # /describe PoiseApplicationPython::Resources::Gunicorn::Resource
58
+
59
+ describe PoiseApplicationPython::Resources::Gunicorn::Provider do
60
+ let(:new_resource) { double('new_resource') }
61
+ let(:test_provider) { described_class.new(new_resource, nil) }
62
+
63
+ describe '#gunicorn_command_options' do
64
+ let(:props) { {} }
65
+ let(:new_resource) { PoiseApplicationPython::Resources::Gunicorn::Resource.new('/test', nil) }
66
+ subject { test_provider.send(:gunicorn_command_options).join(' ') }
67
+ before do
68
+ props.each {|key, value| new_resource.send(key, value) }
69
+ end
70
+
71
+ context 'with defaults' do
72
+ it { is_expected.to eq '--bind 0.0.0.0:80' }
73
+ end # /context with defaults
74
+
75
+ context 'with a config file' do
76
+ let(:props) { {config: '/test/myconfig.py'} }
77
+ it { is_expected.to eq '--config /test/myconfig.py --bind 0.0.0.0:80' }
78
+ end # /context with a config file
79
+
80
+ context 'with a blank config file' do
81
+ let(:props) { {config: ''} }
82
+ it { is_expected.to eq '--bind 0.0.0.0:80' }
83
+ end # /context with a blank config file
84
+
85
+ context 'with two binds' do
86
+ let(:props) { {bind: %w{0.0.0.0:80 0.0.0.0:81}} }
87
+ it { is_expected.to eq '--bind 0.0.0.0:80 --bind 0.0.0.0:81' }
88
+ end # /context with two binds
89
+
90
+ context 'with a config file and preload' do
91
+ let(:props) { {config: '/test/myconfig.py', preload_app: true} }
92
+ it { is_expected.to eq '--config /test/myconfig.py --bind 0.0.0.0:80 --preload' }
93
+ end # /context with a config file and preload
94
+ end # /describe #gunicorn_command_options
95
+ end # /describe PoiseApplicationPython::Resources::Gunicorn::Provider
96
+ end