satorix 0.0.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.gitlab-ci.yml +45 -0
  4. data/.rspec +2 -1
  5. data/.rubocop.yml +11 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +25 -0
  9. data/Procfile +1 -0
  10. data/README.md +93 -1
  11. data/Rakefile +8 -4
  12. data/bin/console +3 -3
  13. data/bin/satorix +8 -0
  14. data/lib/satorix.rb +338 -2
  15. data/lib/satorix/CI/deploy/flynn.rb +129 -0
  16. data/lib/satorix/CI/deploy/flynn/environment_variables.rb +121 -0
  17. data/lib/satorix/CI/deploy/flynn/resources.rb +77 -0
  18. data/lib/satorix/CI/deploy/flynn/routes.rb +266 -0
  19. data/lib/satorix/CI/deploy/flynn/scale.rb +52 -0
  20. data/lib/satorix/CI/shared/buildpack_manager.rb +213 -0
  21. data/lib/satorix/CI/shared/buildpack_manager/buildpack.rb +153 -0
  22. data/lib/satorix/CI/shared/ruby/gem_manager.rb +79 -0
  23. data/lib/satorix/CI/shared/yarn_manager.rb +22 -0
  24. data/lib/satorix/CI/test/python/django_test.rb +35 -0
  25. data/lib/satorix/CI/test/python/safety.rb +27 -0
  26. data/lib/satorix/CI/test/ruby/brakeman.rb +32 -0
  27. data/lib/satorix/CI/test/ruby/bundler_audit.rb +32 -0
  28. data/lib/satorix/CI/test/ruby/cucumber.rb +26 -0
  29. data/lib/satorix/CI/test/ruby/rails_test.rb +26 -0
  30. data/lib/satorix/CI/test/ruby/rspec.rb +26 -0
  31. data/lib/satorix/CI/test/ruby/rubocop.rb +98 -0
  32. data/lib/satorix/CI/test/shared/database.rb +74 -0
  33. data/lib/satorix/shared/console.rb +180 -0
  34. data/lib/satorix/version.rb +1 -1
  35. data/satorix.gemspec +13 -11
  36. data/satorix/CI/deploy/ie_gem_server.rb +76 -0
  37. data/satorix/CI/deploy/rubygems.rb +77 -0
  38. data/satorix/custom.rb +21 -0
  39. metadata +57 -29
  40. data/.travis.yml +0 -5
@@ -0,0 +1,129 @@
1
+ module Satorix
2
+ module CI
3
+ module Deploy
4
+ module Flynn
5
+
6
+ include Satorix
7
+ include Satorix::Shared::Console
8
+
9
+ require_relative 'flynn/environment_variables'
10
+ include EnvironmentVariables
11
+
12
+ require_relative 'flynn/resources'
13
+ include Resources
14
+
15
+ require_relative 'flynn/routes'
16
+ include Routes
17
+
18
+ require_relative 'flynn/scale'
19
+ include Scale
20
+
21
+ extend self
22
+
23
+
24
+ def go
25
+ log_bench('Adding a local reference to the remote Flynn cluster...') { add_cluster }
26
+ log_bench('Creating Flynn project...') { create_project }
27
+ log_bench('Adding Flynn remote to CI local git...') { add_remote }
28
+ log_bench('Adjusting Flynn environment variables...') { adjust_env_vars }
29
+ log_bench('Configuring Flynn inactive slug release count...') { configure_inactive_slug_releases }
30
+ log_bench('Setting resources...') { set_resources }
31
+ log_bench('Deploying to Flynn...') { deploy }
32
+ log_bench('Running Database migrations...') { run_database_migrations } if run_database_migrations?
33
+ log_bench('Scaling application processes...') { adjust_scale }
34
+ log_bench('Setting up routes...') { setup_routes }
35
+ end
36
+
37
+
38
+ def skip_buildpack
39
+ true
40
+ end
41
+
42
+
43
+ def add_cluster
44
+ run_command(['flynn', 'cluster', 'add', '--force', '--default', "--tls-pin=#{ tls_pin }", cluster_name, domain_for_cluster, key], filtered_text: [tls_pin, key])
45
+ end
46
+
47
+
48
+ def add_remote
49
+ run_command(['flynn', '-a', project_name, 'remote', 'add', cluster_name, '-y'])
50
+ end
51
+
52
+
53
+ def cluster_name
54
+ domain_for_cluster
55
+ end
56
+
57
+
58
+ def configure_inactive_slug_releases
59
+ run_command(%w[flynn meta set gc.max_inactive_slug_releases=2])
60
+ end
61
+
62
+
63
+ def create_project
64
+ if project_exists?
65
+ log("Skipping - Flynn project '#{ project_name }' already exists.")
66
+ else
67
+ run_command(['flynn', 'create', "--remote=#{ cluster_name }", '-y', 'project_name'])
68
+ end
69
+ end
70
+
71
+
72
+ def deploy
73
+ run_command(['git', 'push', cluster_name, 'HEAD:refs/heads/master'])
74
+ end
75
+
76
+
77
+ def domain_for_cluster
78
+ ENV["FLYNN_#{ current_branch }_DOMAIN"]
79
+ end
80
+
81
+
82
+ def domain_for_web_host
83
+ "#{ current_branch.downcase }.#{ hosting_namespace }"
84
+ end
85
+
86
+
87
+ def hosting_namespace
88
+ if ENV['SATORIX_HOSTING_NAMESPACE'].to_s !~ only_whitespace
89
+ ENV['SATORIX_HOSTING_NAMESPACE']
90
+ else
91
+ log_error_and_abort("Satorix configuration error: Missing SATORIX_HOSTING_NAMESPACE.\n\nPlease contact support.\n")
92
+ end
93
+
94
+ end
95
+
96
+
97
+ def key
98
+ ENV["FLYNN_#{ current_branch }_KEY"]
99
+ end
100
+
101
+
102
+ def project_exists?
103
+ run_command(%w[flynn apps], quiet: true).split(/\R/).map { |a| a.split.last }.include? project_name
104
+ end
105
+
106
+
107
+ def run_database_migrations?
108
+ [rails_app?, django_app?].any?
109
+ end
110
+
111
+
112
+ def run_database_migrations
113
+ if desired_resource_provider_names.nil? || desired_resource_provider_names.empty?
114
+ log 'Skipping migrations, no database has been defined. Please see the resources section of this log for more information.'
115
+ else
116
+ run_command(%w[flynn run --enable-log bundle exec rake db:migrate]) if rails_app?
117
+ run_command(%w[flynn run --enable-log python public/manage.py migrate]) if django_app?
118
+ end
119
+ end
120
+
121
+
122
+ def tls_pin
123
+ ENV["FLYNN_#{ current_branch }_TLSPIN"]
124
+ end
125
+
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,121 @@
1
+ require 'yaml'
2
+
3
+ module Satorix
4
+ module CI
5
+ module Deploy
6
+ module Flynn
7
+ module EnvironmentVariables
8
+
9
+ def adjust_env_vars
10
+ ensure_required_env_vars_are_defined unless ENV["SATORIX_#{ current_branch }_ENFORCE_ENV_VAR_DEFINITION"] == 'false'
11
+ env_unset
12
+ env_set
13
+ end
14
+
15
+
16
+ def aeev_key_exists?
17
+ !(run_command(%w[flynn env], quiet: true) =~ /#{ exported_aeevs_key }=/mi).nil?
18
+ end
19
+
20
+
21
+ def aeev_prefix
22
+ # AEEV - Application Exportable Environment Variable
23
+ "AEEV_#{ current_branch }_"
24
+ end
25
+
26
+
27
+ def ci_provided_env_vars
28
+ {}.tap do |vars|
29
+ ENV.each do |key, value|
30
+ vars[key.sub(aeev_prefix, '')] = value.sub('***EMPTY_STRING***', '') if key.start_with?(aeev_prefix)
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+ def current_flynn_keys
37
+ aeev_key_exists? ? run_command(['flynn', 'env', 'get', exported_aeevs_key], quiet: true).split : []
38
+ end
39
+
40
+
41
+ def desired_env_vars
42
+ ci_provided_env_vars.tap do |vars|
43
+ vars[exported_aeevs_key] = (vars.keys << exported_aeevs_key).sort.join("\n")
44
+ end
45
+ end
46
+
47
+
48
+ def ensure_required_env_vars_are_defined(configuration_file: 'config/secrets.yml')
49
+ # TODO : handle new Rails secrets methods, like encrypted secrets
50
+ if File.file?(configuration_file)
51
+ secrets = YAML.load_file(configuration_file)
52
+
53
+ all_secrets = {}
54
+ all_secrets.merge!(secrets['shared']) if secrets['shared']
55
+ all_secrets.merge!(secrets['production']) if secrets['production']
56
+
57
+ required = all_secrets.to_yaml.scan(/ENV\[['"](?<var>[A-Z0-9_.]+)['"]\]/).flatten.uniq
58
+ set = ci_provided_env_vars.keys
59
+
60
+ missing = required - set
61
+
62
+ if missing.empty?
63
+ log "All required environment variables from #{ configuration_file } have been defined."
64
+ else
65
+ log_error "Environment variables specified in #{ configuration_file } were not defined for the #{ current_branch.downcase } branch."
66
+ log_error "\nPlease define the following variables in your dashboard:\n#{ missing.join("\n") }\n"
67
+ log_error_and_abort 'Missing required environment variables.'
68
+ end
69
+ else
70
+ log "No #{ configuration_file } exists, skipping environment variable enforcement."
71
+ end
72
+ end
73
+
74
+
75
+ def env_set
76
+ if env_vars_to_set.empty?
77
+ log 'No new environment variables to set.'
78
+ else
79
+ keys_and_values = env_vars_to_set.map { |k, v| "#{ k }=#{ v }" }
80
+ run_command(['flynn', 'env', 'set', keys_and_values].flatten, filtered_text: env_vars_to_set.values)
81
+ end
82
+ end
83
+
84
+
85
+ def env_unset
86
+ if env_vars_to_unset.empty?
87
+ log 'No existing environment variables to unset.'
88
+ else
89
+ run_command(['flynn', 'env', 'unset', env_vars_to_unset])
90
+ end
91
+ end
92
+
93
+
94
+ def env_vars
95
+ @_env_vars ||= begin
96
+ {}.tap do |vars|
97
+ current_flynn_keys.each { |key| vars[key] = run_command(['flynn', 'env', 'get', key], quiet: true).chomp }
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ def env_vars_to_set
104
+ @_env_vars_to_set ||= desired_env_vars.reject { |k, v| env_vars.key?(k) && env_vars[k] == v }
105
+ end
106
+
107
+
108
+ def env_vars_to_unset
109
+ @_env_vars_to_unset ||= env_vars.keys - desired_env_vars.keys
110
+ end
111
+
112
+
113
+ def exported_aeevs_key
114
+ 'AEEV_KEYS'
115
+ end
116
+
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,77 @@
1
+ module Satorix
2
+ module CI
3
+ module Deploy
4
+ module Flynn
5
+ module Resources
6
+
7
+ def add_resources
8
+ if resources_to_add.empty?
9
+ log 'No resources to add.'
10
+ else
11
+ resources_to_add.each { |resource| run_command(['flynn', 'resource', 'add', resource]) }
12
+ end
13
+ end
14
+
15
+
16
+ def available_resources
17
+ %w(postgres mysql mongodb redis)
18
+ end
19
+
20
+
21
+ def current_resource_provider_names
22
+ resource.split("\n").drop(1).map(&:split).map(&:last)
23
+ end
24
+
25
+
26
+ def desired_resource_provider_names
27
+ names = ENV[resource_provider_key].to_s.split
28
+ disallowed = names - available_resources
29
+ unless disallowed.empty?
30
+ log_error_and_abort("Invalid resource#{ 's' if disallowed.length > 1 }: #{ disallowed.join(' ') }")
31
+ end
32
+ names
33
+ end
34
+
35
+
36
+ def remove_resources
37
+ if resources_to_remove.empty?
38
+ log 'No resources to remove.'
39
+ else
40
+ log "The following previously allocated resources are no longer defined in #{ resource_provider_key }:"
41
+ log resources_to_remove.join(' ')
42
+ log ''
43
+ log 'To remove them, use the flynn resource command: https://flynn.io/docs/cli#resource'
44
+ end
45
+ end
46
+
47
+
48
+ def resource
49
+ run_command(%w[flynn resource], quiet: true).chomp
50
+ end
51
+
52
+
53
+ def resource_provider_key
54
+ "FLYNN_#{ current_branch }_RESOURCES"
55
+ end
56
+
57
+
58
+ def resources_to_add
59
+ desired_resource_provider_names - current_resource_provider_names
60
+ end
61
+
62
+
63
+ def resources_to_remove
64
+ current_resource_provider_names - desired_resource_provider_names
65
+ end
66
+
67
+
68
+ def set_resources
69
+ add_resources
70
+ remove_resources
71
+ end
72
+
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,266 @@
1
+ module Satorix
2
+ module CI
3
+ module Deploy
4
+ module Flynn
5
+ module Routes
6
+
7
+ def find_or_create_route(domain)
8
+ route_ids = route_ids(domain)
9
+ if route_ids.empty?
10
+ log "Adding route for #{ domain }..."
11
+ route_ids << fc_route_add(domain)
12
+ log "Route for #{ domain } added with the ID of #{ route_ids.first }."
13
+ else
14
+ multiple = route_ids.length > 1
15
+ log "Route#{ 's' if multiple } already exist#{ 's' unless multiple } for #{ domain }."
16
+ end
17
+
18
+ route_ids
19
+ end
20
+
21
+
22
+ def configure_routes
23
+ defined_and_internal_routes.each do |ddev_id, domain|
24
+ route_ids = find_or_create_route(domain)
25
+ route_ids.each do |route_id|
26
+ add_tls_to_route(route_id: route_id, ddev_id: ddev_id)
27
+ end
28
+ end
29
+ end
30
+
31
+
32
+ def add_tls_to_route(route_id:, ddev_id:)
33
+ domain = defined_and_internal_routes[ddev_id]
34
+ if use_tls?(ddev_id)
35
+ if use_lets_encrypt?(ddev_id)
36
+ log "Using Let's Encrypt for #{ domain }."
37
+ log_error_and_abort "Let's Encrypt support is not implemented, yet!"
38
+ else
39
+ log "Using #{ user_defined_tls?(ddev_id) ? 'custom' : 'default' } certificate details for #{ domain } (#{ service_for_route_id(route_id) } service)."
40
+ File.open('crt', 'w') { |f| f.write(crt_for_ddev_id(ddev_id)) }
41
+ File.open('key', 'w') { |f| f.write(key_for_ddev_id(ddev_id)) }
42
+ fc_route_update(route_id)
43
+ end
44
+ else
45
+ log "Skipping TLS configuration for #{ domain }."
46
+ log "Environment variables #{ env_var_crt_prefix }#{ ddev_id } and #{ env_var_key_prefix }#{ ddev_id } have not been specified."
47
+ log 'For more information, please refer to https://www.satorix.com/docs/user/projects#certificates'
48
+ end
49
+ log ''
50
+ end
51
+
52
+
53
+ def canonical_domain
54
+ ENV["AEEV_#{ current_branch }_SATORIX_CANONICAL_URI_HOST"]
55
+ end
56
+
57
+
58
+ def canonical_domain_information
59
+ "\nThe above routes will all be redirected to the default URL:\n\n\t#{ canonical_uri }\n\n" if canonical_uri?
60
+ end
61
+
62
+
63
+ def canonical_domain_protocol
64
+ ENV["AEEV_#{ current_branch }_SATORIX_CANONICAL_URI_PROTOCOL"]
65
+ end
66
+
67
+
68
+ def canonical_uri
69
+ "#{ canonical_domain_protocol }://#{ canonical_domain }" if canonical_uri?
70
+ end
71
+
72
+
73
+ def canonical_uri?
74
+ [canonical_domain, canonical_domain_protocol].all? { |x| x.to_s !~ only_whitespace }
75
+ end
76
+
77
+
78
+ def crt_for_ddev_id(ddev_id)
79
+ ENV["#{ env_var_crt_prefix }#{ user_defined_tls?(ddev_id) ? ddev_id : 'DEFAULT' }"]
80
+ end
81
+
82
+
83
+ def custom_crt_for_ddev_id?(ddev_id)
84
+ ENV["#{ env_var_crt_prefix }#{ ddev_id }"].to_s !~ only_whitespace
85
+ end
86
+
87
+
88
+ def custom_key_for_ddev_id?(ddev_id)
89
+ ENV["#{ env_var_key_prefix }#{ ddev_id }"].to_s !~ only_whitespace
90
+ end
91
+
92
+
93
+ def defined_and_internal_routes
94
+ defined_routes.merge flynn_internal_routes
95
+ end
96
+
97
+
98
+ def defined_routes
99
+ {}.tap do |routes|
100
+ invalid_keys = flynn_internal_routes.keys
101
+ ENV.each do |key, value|
102
+ next unless key.start_with?(env_var_domain_prefix)
103
+ renamed_key = key.sub(env_var_domain_prefix, '')
104
+ message = "The user-defined route #{ key } conflicts with an internal route."
105
+ log_error_and_abort message if invalid_keys.include?(renamed_key)
106
+ routes[renamed_key] = value
107
+ end
108
+ end
109
+ end
110
+
111
+
112
+ def display_routing_information
113
+ log 'All application routes...'
114
+ log ''
115
+ log urlified_routes_for_display.map { |r| "\t#{ r }" }
116
+ log canonical_domain_information if canonical_uri?
117
+ end
118
+
119
+
120
+ def env_var_crt_prefix
121
+ "CRT_#{ current_branch }_"
122
+ end
123
+
124
+
125
+ def env_var_domain_prefix
126
+ "DDEV_#{ current_branch }_"
127
+ end
128
+
129
+
130
+ def env_var_key_prefix
131
+ "KEY_#{ current_branch }_"
132
+ end
133
+
134
+
135
+ def fc_route
136
+ run_command(['flynn', '-a', project_name, 'route'], quiet: true).chomp
137
+ end
138
+
139
+
140
+ def fc_route_add(domain)
141
+ run_command(['flynn', 'route', 'add', 'http', domain, '--sticky']).chomp
142
+ end
143
+
144
+
145
+ def fc_route_remove(route_id)
146
+ run_command(['flynn', 'route', 'remove', route_id])
147
+ end
148
+
149
+
150
+ def fc_route_update(route_id)
151
+ run_command(['flynn', 'route', 'update', route_id, '--tls-cert=crt', '--tls-key=key', '--sticky'], quiet: true)
152
+ end
153
+
154
+
155
+ def flynn_internal_routes
156
+ { 'FLYNN_INTERNAL' => "#{ project_name }.#{ domain_for_web_host }" }
157
+ end
158
+
159
+
160
+ def key_for_ddev_id(ddev_id)
161
+ ENV["#{ env_var_key_prefix }#{ user_defined_tls?(ddev_id) ? ddev_id : 'DEFAULT' }"]
162
+ end
163
+
164
+
165
+ def only_whitespace
166
+ /\A\s*\z/m
167
+ end
168
+
169
+
170
+ def remove_undefined_routes
171
+ routes_to_remove.each do |route|
172
+ log_header "Removing undefined route #{ route[routes_legend['ROUTE']] }..."
173
+ fc_route_remove route[routes_legend['ID']]
174
+ end
175
+ end
176
+
177
+
178
+ def route_ids(route)
179
+ [].tap do |route_ids|
180
+ routes_all_of('ROUTE').map { |r| r.sub(/https??:/i, '') }.each_with_index do |r, i|
181
+ route_ids << routes_all_of('ID')[i] if r == route
182
+ end
183
+ end
184
+ end
185
+
186
+
187
+ def routes
188
+ routes_with_legend.drop(1)
189
+ end
190
+
191
+
192
+ def routes_all_of(field)
193
+ routes.map { |a| a[routes_legend[field]] }
194
+ end
195
+
196
+
197
+ def routes_legend
198
+ {}.tap { |h| routes_with_legend.first.each_with_index { |route, index| h[route] = index } }
199
+ end
200
+
201
+
202
+ def routes_to_remove
203
+ routes.reject do |route|
204
+ defined_and_internal_routes.values.include? route[routes_legend['ROUTE']].partition(':').last
205
+ end
206
+ end
207
+
208
+
209
+ def routes_with_legend
210
+ fc_route.split("\n").map(&:split)
211
+ end
212
+
213
+
214
+ def service_for_route_id(route_id)
215
+ index = routes_all_of('ID').index(route_id)
216
+ routes_all_of('SERVICE')[index]
217
+ end
218
+
219
+
220
+ def setup_routes
221
+ configure_routes
222
+ remove_undefined_routes
223
+ display_routing_information
224
+ end
225
+
226
+
227
+ def urlified_routes
228
+ routes_all_of('ROUTE').map { |r| urlify_route r }
229
+ end
230
+
231
+
232
+ def urlified_routes_for_display
233
+ urlified_routes.map { |route| urlify_route_for_display route }.sort
234
+ end
235
+
236
+
237
+ def urlify_route(route)
238
+ route.sub(':', '://')
239
+ end
240
+
241
+
242
+ def urlify_route_for_display(route)
243
+ route =~ /^http:/ ? route : "#{ route.sub 'https', 'http' }, #{ route }"
244
+ end
245
+
246
+
247
+ def use_lets_encrypt?(ddev_id)
248
+ crt_for_ddev_id(ddev_id).to_s.gsub(/\W+/, '').casecmp('letsencrypt').zero? &&
249
+ key_for_ddev_id(ddev_id).to_s.gsub(/\W+/, '').casecmp('letsencrypt').zero?
250
+ end
251
+
252
+
253
+ def use_tls?(ddev_id)
254
+ [crt_for_ddev_id(ddev_id), key_for_ddev_id(ddev_id)].all? { |x| x.to_s !~ only_whitespace }
255
+ end
256
+
257
+
258
+ def user_defined_tls?(ddev_id)
259
+ custom_crt_for_ddev_id?(ddev_id) && custom_key_for_ddev_id?(ddev_id)
260
+ end
261
+
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end