orchestration 0.5.11 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -30
  3. data/config/locales/en.yml +11 -15
  4. data/lib/orchestration.rb +4 -0
  5. data/lib/orchestration/docker_compose/app_service.rb +8 -1
  6. data/lib/orchestration/docker_compose/configuration.rb +1 -1
  7. data/lib/orchestration/docker_compose/database_service.rb +1 -1
  8. data/lib/orchestration/docker_compose/install_generator.rb +4 -4
  9. data/lib/orchestration/docker_compose/rabbitmq_service.rb +4 -2
  10. data/lib/orchestration/environment.rb +1 -1
  11. data/lib/orchestration/errors.rb +2 -0
  12. data/lib/orchestration/install_generator.rb +2 -29
  13. data/lib/orchestration/make.rb +4 -0
  14. data/lib/orchestration/make/orchestration.mk +503 -0
  15. data/lib/orchestration/service_check.rb +24 -38
  16. data/lib/orchestration/services/database/adapters.rb +1 -0
  17. data/lib/orchestration/services/database/adapters/adapter_base.rb +21 -0
  18. data/lib/orchestration/services/database/adapters/mysql2.rb +4 -2
  19. data/lib/orchestration/services/database/adapters/postgresql.rb +2 -0
  20. data/lib/orchestration/services/database/adapters/sqlite3.rb +2 -0
  21. data/lib/orchestration/services/database/configuration.rb +17 -19
  22. data/lib/orchestration/services/mixins/configuration_base.rb +1 -1
  23. data/lib/orchestration/services/rabbitmq.rb +1 -0
  24. data/lib/orchestration/templates/Dockerfile.erb +5 -7
  25. data/lib/orchestration/templates/application.mk.erb +23 -7
  26. data/lib/orchestration/templates/rabbitmq.yml.erb +5 -2
  27. data/lib/orchestration/terminal.rb +4 -3
  28. data/lib/orchestration/version.rb +1 -1
  29. data/lib/tasks/orchestration.rake +23 -6
  30. data/orchestration.gemspec +6 -3
  31. metadata +64 -21
  32. data/lib/orchestration/templates/makefile_macros.mk.erb +0 -112
  33. data/lib/orchestration/templates/orchestration.mk.erb +0 -393
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de47b72daeab700a9646fa1c930e747bf7b5f03c630fe63d7ee3fee7aa95d8e3
4
- data.tar.gz: 5a92f1d0826e717fff61ad1b5bf386801907cdf7cf4874a444c5051553f24795
3
+ metadata.gz: cb25cce428ce1b475d757e95164210197a9d737d64b72fc94f2565e2ab8b54a9
4
+ data.tar.gz: 365c9b2705a167cff5ab10a74a3b6a84ef232c9ecbb16c7371519bb76458d973
5
5
  SHA512:
6
- metadata.gz: aba93e5eb3921c239d1a350e007df868b49ae0c038e25dff0db2d188574f55309871b4cf155b6102c135b65ce0e1d39cacffd3837abbca5736150f96a341015f
7
- data.tar.gz: d99a48140ecd9b170b868083dc3023437a4ca212b5ad8f14512f5b2959025988350083b4fd1d6dd96b437a6a360ebc2311cf809b884146db9cc6998d6383afb7
6
+ metadata.gz: 9dd32821fc004fee139bd0de47cae89a5703dd3300f90601ea3c7e7e900cf1c15f1a5ac29d89bc56a21237df3a4e8d04810ec03f9df34c1e3057a3a004f378f5
7
+ data.tar.gz: 56cd182bc0edfdae3a494e6a876f99e42e2d1183e0d24bbd00e853d64b4a9942144913fd024c8fb4ad4b687c8602732916edc0b7d656a21d9e6ff36064bacca7
data/README.md CHANGED
@@ -7,7 +7,7 @@ At its core _Orchestration_ is simply a `Makefile` and a set of `docker-compose.
7
7
  A typical _Rails_ application can be tested, built, pushed to _Docker Hub_, and deployed to _Docker Swarm_ with the following commands:
8
8
 
9
9
  ```bash
10
- make test build push
10
+ make setup test build push
11
11
  make deploy manager=user@swarm.example.com env_file=/var/configs/myapp.env
12
12
  ```
13
13
 
@@ -27,7 +27,7 @@ The below screenshot demonstrates _Orchestration_ being installed in a brand new
27
27
  Add _Orchestration_ to your Gemfile:
28
28
 
29
29
  ```ruby
30
- gem 'orchestration', '~> 0.5.11'
30
+ gem 'orchestration', '~> 0.6.1'
31
31
  ```
32
32
 
33
33
  Install:
@@ -83,7 +83,7 @@ All `make` commands provided by _Orchestration_ (with the exception of `test` an
83
83
  e.g.:
84
84
  ```
85
85
  # Stop all test containers
86
- make stop env=test
86
+ make stop RAILS_ENV=test
87
87
  ```
88
88
 
89
89
  The default value for `env` is `development`.
@@ -91,7 +91,7 @@ The default value for `env` is `development`.
91
91
  As with any `Makefile` targets can be chained together, e.g.:
92
92
  ```
93
93
  # Run tests, build, and push image
94
- make test build push
94
+ make setup test build push
95
95
  ```
96
96
 
97
97
  ### Containers
@@ -113,7 +113,7 @@ make stop
113
113
  #### Interface directly with `docker-compose`
114
114
 
115
115
  ```bash
116
- $(make compose env=test) logs -f database
116
+ $(make compose RAILS_ENV=test) logs -f database
117
117
  ```
118
118
 
119
119
  ### Images
@@ -192,21 +192,15 @@ A default `test` target is provided in your application's main `Makefile`. You a
192
192
 
193
193
  To launch all dependency containers, run database migrations, and run tests:
194
194
  ```bash
195
- make test
195
+ make setup test
196
196
  ```
197
197
 
198
198
  The default `test` command can (and should) be extended. This command is defined in the root `Makefile` in the project and, by defaults, runs `rspec` and `rubocop`.
199
199
 
200
- To run only the `test` command, without test setup (i.e. without restarting containers etc.), pass the `light` option:
201
-
202
- ```bash
203
- make test light=1
204
- ```
200
+ To skip the setup step and just run tests (i.e. once test containers are up and running and ready for use) simply run:
205
201
 
206
- If you prefer to run tests manually (e.g. if you want to run tests for a specific file) then the `test-setup` target can be used:
207
202
  ```bash
208
- make test-setup
209
- bundle exec rspec spec/my_class_spec.rb
203
+ make test
210
204
  ```
211
205
 
212
206
  Note that _Orchestration_ will wait for all services to become fully available (i.e. running and providing valid responses) before attempting to run tests. This is specifically intended to facilitate testing in continuous integration environments.
@@ -216,26 +210,33 @@ _(See [sidecar containers](#sidecar-containers) if you are running your test/dev
216
210
  Dependencies will be launched and then tested for readiness. The retry limit and interval time for readiness tests can be controlled by the following environment variables:
217
211
 
218
212
  ```
219
- ORCHESTRATION_RETRY_LIMIT # default: 10
220
- ORCHESTRATION_RETRY_INTERVAL # default: 5 [seconds]
213
+ ORCHESTRATION_RETRY_LIMIT # default: 15
214
+ ORCHESTRATION_RETRY_INTERVAL # default: 10 [seconds]
221
215
  ```
222
216
 
223
- ### (Local) Production
217
+ ### (Local) Deployment
224
218
 
225
- Run a production environment locally to simulate your deployment platform:
219
+ Run a deployment environment locally to simulate your deployment platform:
226
220
 
227
- ```
228
- make start env=production
221
+ ```bash
222
+ make deploy manager=localhost
229
223
  ```
230
224
 
225
+ Ensure you have passwordless _SSH_ access to your own workstation and that you have approved the host authenticity/fingerprint.
226
+
231
227
  #### Deploy to a remote swarm
232
228
 
233
229
  To connect via _SSH_ to a remote swarm and deploy, pass the `manager` parameter:
234
- ```
230
+ ```bash
235
231
  make deploy manager=user@manager.swarm.example.com
236
232
  ```
237
233
 
238
- The file `orchestration/docker-compose.production.yml` is created automatically. This file will always be used for deployment, regardless of _Rails_ environment. Other environments should be configured using a separate [`.env` file](#env-file) for each environment.
234
+ The file `orchestration/docker-compose.deployment.yml` is created automatically. This file will be used for all deployments, regardless of _Rails_ environment. Other environments should be configured using a separate [`.env` file](#env-file) for each environment. i.e. to deploy a staging environment, create a `staging.env` (for example), set `RAILS_ENV=staging` and run:
235
+ ```bash
236
+ make deploy manager=user@manager.swarm.example.com env_file=staging.env
237
+ ```
238
+
239
+ This way you can set different publish ports and other application configuration variables for each stage you want to deploy to.
239
240
 
240
241
  #### Roll back a deployment
241
242
 
@@ -254,11 +255,11 @@ The `project_name` parameter is also supported.
254
255
 
255
256
  #### Use a custom stack name
256
257
 
257
- The [_Docker_ stack](https://docs.docker.com/engine/reference/commandline/stack/) name defaults to the name of your repository (as defined in `.orchesration.yml`) and the _Rails_ environment, e.g. `anvil_production`.
258
+ The [_Docker_ stack](https://docs.docker.com/engine/reference/commandline/stack/) name defaults to the name of your repository (as defined in `.orchesration.yml`) and the _Rails_ environment, e.g. `anvil_staging`.
258
259
 
259
260
  To override this default, pass the `project_name` parameter:
260
261
  ```
261
- make deploy project_name=acme_anvil_production
262
+ make deploy project_name=custom_stack_name
262
263
  ```
263
264
 
264
265
  This variable will also be available as `COMPOSE_PROJECT_NAME` for use within your `docker-compose.yml`. e.g. to explicitly name a network after the project name:
@@ -300,7 +301,7 @@ tail -f log/orchestration*.log
300
301
  A convenience `Makefile` target `dump` is provided. The following command will output all consumed _stdout_, _stderr_, and _Docker Compose_ container logs for the test environment:
301
302
 
302
303
  ```bash
303
- make dump env=test
304
+ make dump RAILS_ENV=test
304
305
  ```
305
306
 
306
307
  All commands also support the `verbose` flag which will output all logs immediately to the console:
@@ -327,7 +328,7 @@ See related documentation:
327
328
 
328
329
  ## Healthchecks
329
330
 
330
- [Healthchecks](https://docs.docker.com/engine/reference/builder/#healthcheck) are automatically configured for your application. A healthcheck utility is provided in `orchestration/healthcheck.rb`. The following environment variables can be configured (in the `app` service of `orchestration/docker-compose.production.yml`):
331
+ [Healthchecks](https://docs.docker.com/engine/reference/builder/#healthcheck) are automatically configured for your application. A healthcheck utility is provided in `orchestration/healthcheck.rb`. The following environment variables can be configured (in the `app` service of `orchestration/docker-compose.deployment.yml`):
331
332
 
332
333
  | Variable | Meaning | Default Value |
333
334
  |-|-|-|
@@ -355,7 +356,7 @@ Real-world applications will inevitably need to make changes to this file. As wi
355
356
 
356
357
  ## Entrypoint
357
358
 
358
- An [entrypoint](https://docs.docker.com/engine/reference/builder/#entrypoint) script for your application is provided which does the following:
359
+ An [entrypoint](https://docs.docker.com/engine/reference/builder/#entrypoint) script for your application is provided at `orchestration/entrypoint.sh` which does the following:
359
360
 
360
361
  * Runs the `CMD` process as the same system user that launched the container (rather than the default `root` user);
361
362
  * Creates various required temporary directories and removes stale `pid` files;
@@ -370,7 +371,7 @@ If you need to start dependency services (databases, etc.) and connect to them f
370
371
  To do this automatically, pass the `sidecar` parameter to the `start` or `test` targets:
371
372
 
372
373
  ```bash
373
- make test sidecar=1
374
+ make setup test sidecar=1
374
375
  ```
375
376
 
376
377
  When running in sidecar mode container-to-container networking is used so there is no benefit to binding dependency containers to a specific port on the host machine (only the target port will be used). For this reason a random, ephemeral port (chosen by _Docker_) will be used to allow multiple instances of each dependency to run alongside one another.
@@ -381,10 +382,10 @@ Note that a temporary file `orchestration/.sidecar` containing the random projec
381
382
 
382
383
  ```bash
383
384
  # Start dependencies and run tests in sidecar mode
384
- make test sidecar=1
385
+ make setup test sidecar=1
385
386
 
386
387
  # Stop test dependencies in sidecar mode
387
- make stop env=test
388
+ make stop RAILS_ENV=test
388
389
  ```
389
390
 
390
391
  <a name="rabbitmq-configuration"></a>
@@ -402,12 +403,15 @@ _Orchestration_ generates the following `config/rabbitmq.yml`:
402
403
  ```
403
404
  development:
404
405
  url: amqp://127.0.0.1:51070
406
+ management_url: http://127.0.0.1:5069
405
407
 
406
408
  test:
407
409
  url: amqp://127.0.0.1:51068
410
+ management_url: http://127.0.0.1:5067
408
411
 
409
412
  production:
410
413
  url: <%= ENV['RABBITMQ_URL'] %>
414
+ management_url: <%= ENV['RABBITMQ_MANAGEMENT_URL'] %>
411
415
  ```
412
416
 
413
417
  Using this approach, the environment variable `RABBITMQ_URL` can be used to configure _Bunny_ in production (similar to `DATABASE_URL` and `MONGO_URL`).
@@ -1,33 +1,21 @@
1
1
  en:
2
2
  orchestration:
3
- attempt_limit: "Unable to reconnect after %{limit} attempts. Aborting."
3
+ attempt_limit: "Unable to connect after %{limit} attempts. Aborting."
4
4
  default: "default"
5
5
  auto_update: "Orchestration Makefile was automatically updated to the latest version."
6
+ service:
7
+ ready: "%{service} is ready"
6
8
 
7
9
  app:
8
- waiting: "Waiting for app: %{config}"
9
- ready: "App is ready."
10
10
  connection_error: "Error attempting to connect to app: received status code %{code}"
11
11
 
12
12
  database:
13
- waiting: "Waiting for database: %{config}"
14
- ready: "Database is ready."
15
13
  unknown_environment: "Environment not defined in database configuration: %{environment}"
16
14
  unknown_adapter: "Unable to determine adapter from database.yml: %{adapter}"
17
15
 
18
16
  mongo:
19
- waiting: "Waiting for Mongo: %{config}"
20
- ready: "Mongo is ready."
21
17
  bad_config: "Unable to parse Mongo config: %{path}. Expected section for one of: %{expected}"
22
18
 
23
- rabbitmq:
24
- waiting: "Waiting for RabbitMQ: %{config}"
25
- ready: "RabbitMQ is ready."
26
-
27
- custom_service:
28
- waiting: "Waiting for [%{service}]: %{config}"
29
- ready: "[%{service}] is ready."
30
-
31
19
  settings:
32
20
  docker:
33
21
  organization:
@@ -44,3 +32,11 @@ en:
44
32
  install: "Install Orchestration tools"
45
33
  install_makefile: "(Re)create orchestration/Makefile"
46
34
  wait: "Wait for development/test dependencies to be available"
35
+ db:
36
+ url: "Return the database URL for the current environment (RAILS_ENV)"
37
+ console: "Launch a database console for the current environment (RAILS_ENV)"
38
+
39
+ dbconsole:
40
+ postgresql: "PGPASSWORD='%<password>s' psql --username=%<username>s --host=%<host>s --port=%<port>s --dbname=%<database>s"
41
+ sqlite3: "sqlite3 %<database>s"
42
+ mysql2: "mysql --user=%<username>s --port=%<port>s --host=%<host>s --password=%<password>s --no-auto-rehash %<database>s"
data/lib/orchestration.rb CHANGED
@@ -36,6 +36,10 @@ module Orchestration
36
36
  Pathname.new(File.dirname(__dir__))
37
37
  end
38
38
 
39
+ def self.makefile
40
+ root.join('lib', 'orchestration', 'make', 'orchestration.mk')
41
+ end
42
+
39
43
  def self.rakefile
40
44
  root.join('lib', 'Rakefile')
41
45
  end
@@ -99,8 +99,15 @@ module Orchestration
99
99
  'RAILS_SERVE_STATIC_FILES' => '1',
100
100
  'WEB_PRELOAD_APP' => '1',
101
101
  'WEB_HEALTHCHECK_PATH' => '/',
102
+ 'WEB_PORT' => 8080,
102
103
  'DATABASE_URL' => database_url
103
- }.merge(Hash[inherited_environment.map { |key| [key, nil] }])
104
+ }.merge(inherited_environment.map { |key| [key, nil] }.to_h).merge(rabbitmq_urls)
105
+ end
106
+
107
+ def rabbitmq_urls
108
+ return {} unless Services::RabbitMQ::Configuration.new(Environment.new).enabled?
109
+
110
+ { 'RABBITMQ_URL' => 'amqp://rabbitmq:5672', 'RABBITMQ_MANAGEMENT_URL' => 'http://rabbitmq:15672' }
104
111
  end
105
112
 
106
113
  def database_url
@@ -14,7 +14,7 @@ module Orchestration
14
14
  end
15
15
 
16
16
  def services
17
- Hash[services_enabled]
17
+ services_enabled.to_h
18
18
  end
19
19
 
20
20
  def volumes
@@ -34,7 +34,7 @@ module Orchestration
34
34
  end
35
35
 
36
36
  def networks
37
- return {} unless @environment == :production
37
+ return {} unless @environment == :deployment
38
38
 
39
39
  { 'local' => { 'aliases' => ['database-local'] } }
40
40
  end
@@ -18,8 +18,8 @@ module Orchestration
18
18
  create_compose_file(:development)
19
19
  end
20
20
 
21
- def docker_compose_production_yml
22
- create_compose_file(:production)
21
+ def docker_compose_deployment_yml
22
+ create_compose_file(:deployment)
23
23
  end
24
24
 
25
25
  def enabled_services(environment)
@@ -60,7 +60,7 @@ module Orchestration
60
60
  DockerCompose::Configuration.new(
61
61
  @env,
62
62
  environment,
63
- Hash[configurations(environment)]
63
+ configurations(environment).to_h
64
64
  )
65
65
  end
66
66
 
@@ -68,7 +68,7 @@ module Orchestration
68
68
  case environment
69
69
  when :test, :development
70
70
  %i[database mongo rabbitmq]
71
- when :production
71
+ when :deployment
72
72
  %i[app database mongo rabbitmq]
73
73
  when :local, nil
74
74
  []
@@ -13,15 +13,17 @@ module Orchestration
13
13
  def definition
14
14
  return nil unless @config.enabled?
15
15
 
16
- { 'image' => 'library/rabbitmq' }.merge(ports)
16
+ { 'image' => 'library/rabbitmq:management' }.merge(ports)
17
17
  end
18
18
 
19
19
  def ports
20
20
  return {} unless %i[development test].include?(@environment)
21
21
 
22
22
  container_port = Orchestration::Services::RabbitMQ::PORT
23
+ management_port = Orchestration::Services::RabbitMQ::MANAGEMENT_PORT
23
24
 
24
- { 'ports' => ["#{sidecar_port(@environment)}#{container_port}"] }
25
+ { 'ports' => ["#{sidecar_port(@environment)}#{container_port}",
26
+ "#{sidecar_port(@environment)}#{management_port}"] }
25
27
  end
26
28
  end
27
29
  end
@@ -129,7 +129,7 @@ module Orchestration
129
129
  def docker_filter(string)
130
130
  # Filter out characters not accepted by Docker Hub
131
131
  permitted = [('0'..'9'), ('a'..'z')].map(&:to_a).flatten
132
- string.split('').select { |char| permitted.include?(char) }.join
132
+ string.chars.select { |char| permitted.include?(char) }.join
133
133
  end
134
134
  end
135
135
  end
@@ -4,7 +4,9 @@ module Orchestration
4
4
  class OrchestrationError < StandardError; end
5
5
 
6
6
  class HTTPConnectionError < OrchestrationError; end
7
+
7
8
  class DatabaseConfigurationError < OrchestrationError; end
9
+
8
10
  class MongoConfigurationError < OrchestrationError; end
9
11
 
10
12
  class UnknownEnvironmentError < DatabaseConfigurationError; end
@@ -27,30 +27,9 @@ module Orchestration
27
27
  @terminal.write(:skip, relpath)
28
28
  end
29
29
 
30
- def verify_makefile(skip: true)
31
- # Only run when called explicitly [from Rake tasks].
32
- # (I know this is hacky).
33
- return if skip
34
-
35
- content = template('orchestration.mk', makefile_environment)
36
- path = @env.orchestration_root.join('Makefile')
37
- return if path.exist? && content == File.read(path)
38
-
39
- write_file(path, content)
40
- @terminal.write(:update, 'Makefile')
41
- @terminal.write(:status, t(:auto_update))
42
- end
43
-
44
30
  def application_makefile
45
31
  path = @env.root.join('Makefile')
46
32
  simple_copy('application.mk', path) unless File.exist?(path)
47
- inject_if_missing(path, 'include orchestration/Makefile')
48
- end
49
-
50
- def orchestration_makefile
51
- content = template('orchestration.mk', makefile_environment)
52
- path = @env.orchestration_root.join('Makefile')
53
- path.exist? ? update_file(path, content) : create_file(path, content)
54
33
  end
55
34
 
56
35
  def dockerfile
@@ -71,7 +50,7 @@ module Orchestration
71
50
  def docker_compose
72
51
  @docker_compose.docker_compose_test_yml
73
52
  @docker_compose.docker_compose_development_yml
74
- @docker_compose.docker_compose_production_yml
53
+ @docker_compose.docker_compose_deployment_yml
75
54
  end
76
55
 
77
56
  def puma
@@ -133,14 +112,8 @@ module Orchestration
133
112
  I18n.t("orchestration.#{key}")
134
113
  end
135
114
 
136
- def makefile_environment
137
- macros = template('makefile_macros.mk', env: @env)
138
-
139
- { env: @env, services: enabled_services, macros: macros }
140
- end
141
-
142
115
  def enabled_services
143
- %i[test development production].map do |environment|
116
+ %i[test development deployment].map do |environment|
144
117
  @docker_compose.enabled_services(environment)
145
118
  end.flatten.uniq
146
119
  end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'orchestration'
4
+ puts Orchestration.makefile
@@ -0,0 +1,503 @@
1
+
2
+ ### Environment setup ###
3
+ SHELL:=/bin/bash
4
+ MAKE:=mkpath=${mkpath} make --no-print-directory
5
+
6
+ TERM ?= 'dumb'
7
+ pwd:=$(shell pwd)
8
+
9
+ orchestration_dir_name=orchestration
10
+ orchestration_dir=orchestration
11
+
12
+ ifdef env_file
13
+ custom_env_file ?= 1
14
+ else
15
+ custom_env_file ?= 0
16
+ endif
17
+
18
+ ifneq (,$(wildcard ${pwd}/config/database.yml))
19
+ database_enabled = 1
20
+ else
21
+ database_enabled = 0
22
+ endif
23
+
24
+ red:=$(shell tput setaf 1)
25
+ green:=$(shell tput setaf 2)
26
+ yellow:=$(shell tput setaf 3)
27
+ blue:=$(shell tput setaf 4)
28
+ magenta:=$(shell tput setaf 5)
29
+ cyan:=$(shell tput setaf 6)
30
+ gray:=$(shell tput setaf 7)
31
+ reset:=$(shell tput sgr0)
32
+ tick=[${green}✓${reset}]
33
+ cross=[${red}✘${reset}]
34
+
35
+ make=$(MAKE) $1
36
+ orchestration_config_filename:=.orchestration.yml
37
+ orchestration_config:=${pwd}/${orchestration_config_filename}
38
+ system_prefix=${reset}[${cyan}exec${reset}]
39
+ warn_prefix=${reset}[${yellow}warn${reset}]
40
+ echo_prefix=${reset}[${blue}info${reset}]
41
+ fail_prefix=${reset}[${red}fail${reset}]
42
+ logs_prefix=${reset}[${green}logs${reset}]
43
+ system=echo '${system_prefix} ${cyan}$1${reset}'
44
+ warn=echo '${warn_prefix} ${reset}$1${reset}'
45
+ echo=echo '${echo_prefix} ${reset}$1${reset}'
46
+ fail=echo '${fail_prefix} ${reset}$1${reset}'
47
+ logs=echo '${logs_prefix} ${reset}$1${reset}'
48
+ print_error=printf '${red}\#${reset} '$1 | tee '${stderr}'
49
+ println_error=$(call print_error,$1'\n')
50
+ print=printf '${blue}\#${reset} '$1
51
+ println=$(call print,$1'\n')
52
+ printraw=printf $1
53
+ stdout=${pwd}/log/orchestration.stdout.log
54
+ stderr=${pwd}/log/orchestration.stderr.log
55
+ log_path_length=$(shell echo "${stdout}" | wc -c)
56
+ ifndef verbose
57
+ log_tee:= 2>&1 | tee -a ${stdout}
58
+ log:= >>${stdout} 2>>${stderr}
59
+ progress_point:=perl -e 'printf("[${magenta}busy${reset}] "); while( my $$line = <STDIN> ) { printf("."); select()->flush(); }'
60
+ log_progress:= > >(tee -ai ${stdout} >&1 | ${progress_point}) 2> >(tee -ai ${stderr} 2>&1 | ${progress_point})
61
+ endif
62
+ hr=$(call println,"$1$(shell head -c ${log_path_length} < /dev/zero | tr '\0' '=')${reset}")
63
+ managed_env_tag:=\# -|- ORCHESTRATION
64
+ standard_env_path:=${pwd}/.env
65
+ backup_env_path:=${pwd}/.env.orchestration.backup
66
+ is_managed_env:=$$(test -f '${standard_env_path}' && tail -n 1 '${standard_env_path}') == "${managed_env_tag}"*
67
+ token:=$(shell cat /dev/urandom | LC_CTYPE=C tr -dc 'a-z0-9' | fold -w8 | head -n1)
68
+ back_up_env:=( \
69
+ [ ! -f '${standard_env_path}' ] \
70
+ || \
71
+ ( \
72
+ [ -f '${standard_env_path}' ] \
73
+ && cp '${standard_env_path}' '${backup_env_path}' \
74
+ ) \
75
+ )
76
+
77
+ key_chars:=[a-zA-Z0-9_]
78
+ censored:=**********
79
+ censor=s/\(^${key_chars}*$(1)${key_chars}*\)=\(.*\)$$/\1=${censored}/
80
+ censor_urls:=s|\([a-zA-Z0-9_+]\+://.*:\).*\(@.*\)$$|\1${censored}\2|
81
+ format_env:=sed '$(call censor,SECRET); \
82
+ $(call censor,TOKEN); \
83
+ $(call censor,PRIVATE); \
84
+ $(call censor,KEY); \
85
+ $(censor_urls); \
86
+ /^\s*$$/d; \
87
+ /^\s*\#/d; \
88
+ s/\(^[a-zA-Z0-9_]\+\)=/${blue}\1${reset}=/; \
89
+ s/^/ /; \
90
+ s/=\(.*\)$$/=${yellow}\1${reset}/' | \
91
+ sort
92
+
93
+ exit_fail=( \
94
+ $(call printraw,' ${cross}') ; \
95
+ $(call make,dump src_cmd=$(MAKECMDGOALS)) ; \
96
+ echo ; \
97
+ $(call println,'Failed. ${cross}') ; \
98
+ exit 1 \
99
+ )
100
+
101
+ ifdef env_file
102
+ -include ${env_file}
103
+ else
104
+ ifneq (${env},test)
105
+ ifeq (,$(findstring test,$(MAKECMDGOALS)))
106
+ -include .env
107
+ endif
108
+ endif
109
+ endif
110
+
111
+ export
112
+
113
+ ifneq (,$(findstring test,$(MAKECMDGOALS)))
114
+ env=test
115
+ endif
116
+
117
+ ifneq (,$(env))
118
+ # `env` set by current shell.
119
+ else ifneq (,$(RAILS_ENV))
120
+ env=$(RAILS_ENV)
121
+ else ifneq (,$(RACK_ENV))
122
+ env=$(RACK_ENV)
123
+ else
124
+ env=development
125
+ endif
126
+
127
+ env_human=${gray}${env}${reset}
128
+ DOCKER_TAG ?= latest
129
+
130
+ ifneq (,$(wildcard ./Gemfile))
131
+ bundle_cmd = bundle exec
132
+ endif
133
+ rake=DEVPACK_DISABLE=1 RACK_ENV=${env} RAILS_ENV=${env} ${bundle_cmd} rake
134
+
135
+ ifneq (,$(wildcard ${env_file}))
136
+ rake_cmd:=${rake}
137
+ rake=. ${env_file} && ${rake_cmd}
138
+ endif
139
+
140
+ docker_config:=$(shell DEVPACK_DISABLE=1 RAILS_ENV=development ${bundle_cmd} rake orchestration:config 2>/dev/null || echo no-org no-repo)
141
+ docker_organization=$(word 1,$(docker_config))
142
+ docker_repository=$(word 2,$(docker_config))
143
+
144
+ ifeq (,$(project_name))
145
+ project_base = ${docker_repository}_${env}
146
+ else
147
+ project_base := $(project_name)
148
+ endif
149
+
150
+ ifeq (,$(findstring deploy,$(MAKECMDGOALS)))
151
+ sidecar_suffix := $(shell test -f ${orchestration_dir}/.sidecar && cat ${orchestration_dir}/.sidecar)
152
+ ifneq (,${sidecar_suffix})
153
+ sidecar := 1
154
+ endif
155
+
156
+ ifdef sidecar
157
+ # Set the variable to an empty string so that "#{sidecar-1234}" will
158
+ # evaluate to "1234" in port mappings.
159
+ sidecar_compose = sidecar=''
160
+ ifeq (,${sidecar_suffix})
161
+ sidecar_suffix := $(call token)
162
+ _ignore := $(shell echo ${sidecar_suffix} > ${orchestration_dir}/.sidecar)
163
+ endif
164
+
165
+ ifeq (,${sidecar_suffix})
166
+ $(warning Unable to generate project suffix; project name collisions may occur.)
167
+ endif
168
+ compose_project_name = ${project_base}_${sidecar_suffix}
169
+ else
170
+ compose_project_name = ${project_base}
171
+ endif
172
+ else
173
+ compose_project_name = ${project_base}
174
+ endif
175
+
176
+ compose_base=env -i \
177
+ PATH=$(PATH) \
178
+ HOST_UID=$(shell id -u) \
179
+ DOCKER_ORGANIZATION="${docker_organization}" \
180
+ DOCKER_REPOSITORY="${docker_repository}" \
181
+ COMPOSE_PROJECT_NAME="${compose_project_name}" \
182
+ ${sidecar_compose} \
183
+ docker-compose \
184
+ -f ${orchestration_dir}/docker-compose.${env}.yml
185
+
186
+ git_branch ?= $(if $(branch),$(branch),$(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo no-branch))
187
+ ifndef dev
188
+ git_version ?= $(shell git rev-parse --short --verify ${git_branch} 2>/dev/null || echo no-version)
189
+ else
190
+ git_version = dev
191
+ endif
192
+
193
+ docker_image=${docker_organization}/${docker_repository}:${git_version}
194
+
195
+ compose=${compose_base}
196
+ compose_human=docker-compose -f ${orchestration_dir_name}/docker-compose.${env}.yml
197
+ random_str=cat /dev/urandom | LC_ALL=C tr -dc 'a-z' | head -c $1
198
+
199
+ ifneq (,$(wildcard ${orchestration_dir}/docker-compose.local.yml))
200
+ compose:=${compose} -f ${orchestration_dir}/docker-compose.local.yml
201
+ endif
202
+
203
+ all: build
204
+
205
+ ### Container management commands ###
206
+
207
+ .PHONY: start
208
+ ifndef network
209
+ start: network := ${compose_project_name}
210
+ endif
211
+ start: _create-log-directory _clean-logs
212
+ @$(call system,${compose_human} up --detach)
213
+ ifeq (${env},$(filter ${env},test development))
214
+ @${compose} up --detach --force-recreate --renew-anon-volumes --remove-orphans ${services} ${log} || ${exit_fail}
215
+ @[ -n '${sidecar}' ] && \
216
+ ( \
217
+ $(call echo,(joining dependency network ${cyan}${network}${reset})) ; \
218
+ $(call system,docker network connect "${network}") ; \
219
+ docker network connect '${network}' '$(shell hostname)' ${log} \
220
+ || ( \
221
+ $(call warn,Unable to join network: "${cyan}${network}${reset}". Container will not be able to connect to dependency services) ; \
222
+ $(call echo,Try deleting "${cyan}orchestration/.sidecar${reset}" if you do not want to use sidecar mode) ; \
223
+ ) \
224
+ ) \
225
+ || ( [ -z '${sidecar}' ] || ${exit_fail} )
226
+ else
227
+ @${compose} up --detach --scale app=$${instances:-1} ${log} || ${exit_fail}
228
+ endif
229
+ @$(call echo,${env_human} containers started ${tick})
230
+ @$(call echo,Waiting for services to become available)
231
+ @$(call make,wait) 2>${stderr} || ${exit_fail}
232
+
233
+ .PHONY: stop
234
+ stop: network := ${compose_project_name}
235
+ stop:
236
+ @$(call echo,Stopping ${env_human} containers)
237
+ @$(call system,${compose_human} down)
238
+ @if docker ps --format "{{.ID}}" | grep -q $(shell hostname) ; \
239
+ then \
240
+ ( docker network disconnect ${network} $(shell hostname) ${log} || : ) \
241
+ && \
242
+ ( ${compose} down ${log} || ${exit_fail} ) ; \
243
+ else \
244
+ ${compose} down ${log} || ${exit_fail} ; \
245
+ fi
246
+ @$(call echo,${env_human} containers stopped ${tick})
247
+
248
+ .PHONY: logs
249
+ logs:
250
+ @${compose} logs -f
251
+
252
+ .PHONY: config
253
+ config:
254
+ @${compose} config
255
+
256
+ .PHONY: compose
257
+ compose:
258
+ @echo ${compose}
259
+
260
+ ### Development/Test Utility Commands
261
+
262
+ .PHONY: serve
263
+ serve: env_file ?= ./.env
264
+ serve: rails = RAILS_ENV='${env}' bundle exec rails server ${server}
265
+ serve:
266
+ @if [ -f "${env_file}" ] ; \
267
+ then ( \
268
+ $(call echo,Environment${reset}: ${cyan}${env_file}${reset}) && \
269
+ cat '${env_file}' | ${format_env} \
270
+ ) ; \
271
+ fi
272
+ ${rails}
273
+
274
+ .PHONY: console
275
+ console: env_file ?= ./.env
276
+ console: rails = RAILS_ENV='${env}' bundle exec rails
277
+ console:
278
+ @if [ -f "${env_file}" ] ; \
279
+ then ( \
280
+ $(call echo,Environment${reset}: ${cyan}${env_file}${reset}) && \
281
+ cat '${env_file}' | ${format_env} \
282
+ ) ; \
283
+ fi
284
+ ${rails} console
285
+
286
+ .PHONY: db-console
287
+ db-console:
288
+ @${rake} orchestration:db:console RAILS_ENV=${env}
289
+
290
+ .PHONY: setup
291
+ setup: url = $(shell ${rake} orchestration:db:url RAILS_ENV=${env})
292
+ setup: _log-notify
293
+ @$(call echo,Setting up ${env_human} environment)
294
+ @$(call make,start env=${env})
295
+ ifneq (,$(wildcard config/database.yml))
296
+ @$(call echo,Preparing ${env_human} database)
297
+ @$(call system,rake db:create DATABASE_URL="${url}")
298
+ @${rake} db:create RAILS_ENV=${env} ${log} || : ${log}
299
+ ifneq (,$(wildcard db/structure.sql))
300
+ @$(call system,rake db:schema:load DATABASE_URL="${url}")
301
+ @${rake} db:schema:load DATABASE_URL='${url}' ${log} || ${exit_fail}
302
+ else ifneq (,$(wildcard db/schema.rb))
303
+ @$(call system,rake db:schema:load DATABASE_URL="${url}")
304
+ @${rake} db:schema:load DATABASE_URL='${url}' ${log} || ${exit_fail}
305
+ endif
306
+ @$(call system,rake db:migrate DATABASE_URL="${url}")
307
+ @${rake} db:migrate RAILS_ENV=${env}
308
+ endif
309
+ @$(MAKE) -n post-setup >/dev/null 2>&1 \
310
+ && $(call system,make post-setup RAILS_ENV=${env}) \
311
+ && $(MAKE) post-setup RAILS_ENV=${env}
312
+ @$(call echo,${env_human} environment setup complete ${tick})
313
+
314
+ .PHONY: dump
315
+ dump:
316
+ ifndef verbose
317
+ @$(call println)
318
+ @$(call println,'${yellow}Captured${reset} ${green}stdout${reset} ${yellow}and${reset} ${red}stderr${reset} ${yellow}log data [${cyan}${env}${yellow}]${reset}:')
319
+ @$(call println)
320
+ @echo
321
+ @test -f '${stdout}' && ( \
322
+ $(call hr,${green}) ; \
323
+ $(call println,'${gray}${stdout}${reset}') ; \
324
+ $(call hr,${green}) ; \
325
+ echo ; cat '${stdout}' ; echo ; \
326
+ $(call hr,${green}) ; \
327
+ )
328
+
329
+ @test -f '${stdout}' && ( \
330
+ echo ; \
331
+ $(call hr,${red}) ; \
332
+ $(call println,'${gray}${stderr}${reset}') ; \
333
+ $(call hr,${red}) ; \
334
+ echo ; cat '${stderr}' ; echo ; \
335
+ $(call hr,${red}) ; \
336
+ )
337
+ endif
338
+ ifneq (build,${src_cmd})
339
+ ifneq (push,${src_cmd})
340
+ @echo ; \
341
+ $(call hr,${yellow}) ; \
342
+ $(call println,'${gray}docker-compose logs${reset}') ; \
343
+ $(call hr,${yellow}) ; \
344
+ echo
345
+ @${compose} logs
346
+ @echo ; \
347
+ $(call hr,${yellow})
348
+ @$(NOOP)
349
+ endif
350
+ endif
351
+
352
+ .PHONY: tag
353
+ tag:
354
+ @echo ${docker_image}
355
+
356
+ ### Deployment utility commands ###
357
+
358
+ .PHONY: deploy
359
+ deploy: _log-notify _clean-logs
360
+ ifdef env_file
361
+ deploy: env_file_option = --env-file ${env_file}
362
+ endif
363
+ deploy: RAILS_ENV := ${env}
364
+ deploy: RACK_ENV := ${env}
365
+ deploy: DOCKER_TAG = ${git_version}
366
+ deploy: base_vars = DOCKER_ORGANIZATION=${docker_organization} DOCKER_REPOSITORY=${docker_repository} DOCKER_TAG=${git_version}
367
+ deploy: compose_deploy := ${base_vars} COMPOSE_PROJECT_NAME=${project_base} HOST_UID=$(shell id -u) docker-compose ${env_file_option} --project-name ${project_base} -f orchestration/docker-compose.deployment.yml
368
+ deploy: config_cmd = ${compose_deploy} config
369
+ deploy: remote_cmd = cat | docker stack deploy --prune --with-registry-auth -c - ${project_base}
370
+ deploy: ssh_cmd = ssh "${manager}"
371
+ deploy: deploy_cmd := ${config_cmd} | ${ssh_cmd} "/bin/bash -lc '${remote_cmd}'"
372
+ deploy:
373
+ ifndef manager
374
+ @$(call fail,Missing ${cyan}manager${reset} parameter: ${cyan}make deploy manager=swarm-manager.example.com${reset}) ; exit 1
375
+ endif
376
+ @$(call echo,Deploying ${env_human} stack via ${cyan}${manager}${reset} as ${cyan}${project_base}${reset}) && \
377
+ ( \
378
+ ( test -f '${env_file}' && $(call echo,Deployment environment:) && cat '${env_file}' | ${format_env} || : ) && \
379
+ $(call echo,Application image: ${cyan}${docker_image}${reset}) ; \
380
+ $(call system,${config_cmd} | ${ssh_cmd} "/bin/bash -lc '\''${remote_cmd}'\''") ; \
381
+ ${deploy_cmd} \
382
+ )
383
+ @$(call echo,Deployment ${green}complete${reset} ${tick})
384
+
385
+ .PHONY: rollback
386
+ ifndef service
387
+ rollback: service = app
388
+ endif
389
+ rollback:
390
+ ifndef manager
391
+ @$(call fail,Missing `manager` parameter: `make deploy manager=swarm-manager.example.com`)
392
+ @exit 1
393
+ endif
394
+ @$(call echo,Rolling back ${cyan}${compose_project_name}_${service}${reset} via ${cyan}${manager}${reset} ...)
395
+ @$(call system,docker service rollback --detach "${compose_project_name}_${service}")
396
+ @ssh "${manager}" 'docker service rollback --detach "${compose_project_name}_${service}"' ${log} || ${exit_fail}
397
+ @$(call echo,Rollback request ${green}complete${reset} ${tick})
398
+
399
+ ### Service healthcheck commands ###
400
+
401
+ .PHONY: wait
402
+ wait:
403
+ @${rake} orchestration:wait
404
+ @$(call echo,${env_human} services ${green}ready${reset} ${tick})
405
+
406
+ ## Generic Listener healthcheck for TCP services ##
407
+
408
+ wait-listener:
409
+ @${rake} orchestration:listener:wait service=${service} sidecar=${sidecar}
410
+
411
+ ### Docker build commands ###
412
+
413
+ .PHONY: build
414
+ build: _log-notify _clean-logs
415
+ build: build_dir = ${orchestration_dir}/.build
416
+ build: context = ${build_dir}/context.tar
417
+ build: build_args := --build-arg GIT_COMMIT='${git_version}'
418
+ build: tag_human = ${cyan}${docker_organization}/${docker_repository}:${git_version}${reset}
419
+ build: latest_tag_human = ${cyan}${docker_organization}/${docker_repository}:latest${reset}
420
+ ifdef BUNDLE_GITHUB__COM
421
+ build: build_args := ${build_args} --build-arg BUNDLE_GITHUB__COM
422
+ endif
423
+ ifdef BUNDLE_BITBUCKET__ORG
424
+ build: build_args := ${build_args} --build-arg BUNDLE_BITBUCKET__ORG
425
+ endif
426
+ build: _create-log-directory check-local-changes
427
+ @$(call echo,Preparing build context from ${cyan}${git_branch}${reset} (${cyan}${git_version}${reset})${reset})
428
+ @$(call system,git archive --format "tar" -o "${context}" "${git_branch}")
429
+ @mkdir -p ${orchestration_dir}/.build ${log} || ${exit_fail}
430
+ ifndef dev
431
+ @git show ${git_branch}:./Gemfile > ${orchestration_dir}/.build/Gemfile 2>${stderr} || ${exit_fail}
432
+ @git show ${git_branch}:./Gemfile.lock > ${orchestration_dir}/.build/Gemfile.lock 2>${stderr} || ${exit_fail}
433
+ @git archive --format 'tar' -o '${context}' '${git_branch}' ${log} || ${exit_fail}
434
+ else
435
+ @tar -cvf '${context}' . ${log} || ${exit_fail}
436
+ endif
437
+ ifdef include
438
+ @$(call echo,Including files from: ${cyan}${include}${reset})
439
+ @(while read line; do \
440
+ _system () { echo '${system_prefix}' $$1 }
441
+ export line; \
442
+ include_dir="${build_dir}/$$(dirname "$${line}")/" && \
443
+ mkdir -p "$${include_dir}" && cp "$${line}" "$${include_dir}" \
444
+ && (cd '${orchestration_dir}/.build/' && tar rf 'context.tar' "$${line}"); \
445
+ _system "tar rf 'context.tar' '$${line}'")
446
+ done < '${include}') ${log} || ${exit_fail}
447
+ @$(call echo,Build context ${green}ready${reset} ${tick})
448
+ endif
449
+ @$(call echo,Building image ${tag_human})
450
+ @$(call system,docker build ${build_args} -t ${docker_organization}/${docker_repository}:${git_version} ${orchestration_dir}/)
451
+ @docker build ${build_args} \
452
+ -t ${docker_organization}/${docker_repository}:${git_version} \
453
+ ${orchestration_dir}/ ${log_progress} || ${exit_fail}
454
+ @echo
455
+ @$(call echo,Build ${green}complete${reset} ${tick})
456
+ @$(call echo,[${green}tag${reset}] ${tag_human})
457
+
458
+ .PHONY: push
459
+ push: _log-notify _clean-logs
460
+ @$(call echo,Pushing ${cyan}${docker_image}${reset} to registry)
461
+ @$(call system,docker push ${docker_image})
462
+ @docker push ${docker_image} ${log_progress} || ${exit_fail}
463
+ @echo
464
+ @$(call echo,Push ${green}complete${reset} ${tick})
465
+
466
+ .PHONY: check-local-changes
467
+ check-local-changes:
468
+ ifndef dev
469
+ @changes="$$(git status --porcelain)"; if ! [ -z "${changes}" ] && [[ "${changes}" != "?? orchestration/.sidecar" ]]; \
470
+ then \
471
+ $(call warn,You have uncommitted changes which will not be included in your build:) ; \
472
+ git status --porcelain ; \
473
+ $(call echo,Commit these changes to Git or, alternatively, build in development mode to test your changes before committing: ${cyan}make build dev=1${reset}) ; \
474
+ fi
475
+ endif
476
+
477
+ ### Internal Commands ###
478
+ #
479
+ .PHONY: _log-notify
480
+ _log-notify: comma=,
481
+ _log-notify: _verify-repository
482
+ ifndef verbose
483
+ @$(call logs,${green}stdout${reset}: ${cyan}log/orchestration.stdout.log${reset}${comma} ${red}stderr${reset}: ${cyan}log/orchestration.stderr.log)
484
+ endif
485
+
486
+ .PHONY: _verify-repository
487
+ _verify-repository:
488
+ @if ! git rev-parse HEAD >/dev/null 2>&1 ; then $(call fail,You must make at least one commit before you can use Orchestration commands) ; exit 1 ; fi
489
+
490
+ .PHONY: _clean-logs
491
+ _clean-logs:
492
+ _clean-logs: _create-log-directory
493
+ @rm -f '${stdout}' '${stderr}'
494
+ @touch '${stdout}' '${stderr}'
495
+
496
+ .PHONY: _create-log-directory
497
+ _create-log-directory:
498
+ @mkdir -p log
499
+
500
+ # Used by Orchestration test suite to verify Makefile syntax
501
+ .PHONY: _test
502
+ _test:
503
+ @echo 'test command'