procsd 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a029c6f4d0953cc9b21f3508a14e9057a4386fd53c786167737a9097c4175f13
4
- data.tar.gz: 2f3fa1f897cb3bbf65aa6e00c1b8fbc0c3f1bde13d5dc559514633404ef691ce
3
+ metadata.gz: 862f5699d79e6ac90e5c946ba08d13b190bde81c1c2f015bd9aea7ca2b797d2e
4
+ data.tar.gz: 4713b4d70b167bb999727bb20869568a6e15e3c91d9197f4c062eb4e384b9c9b
5
5
  SHA512:
6
- metadata.gz: 6f5a266a604bdbc1cbd9519b510a1eabf801d753d8497729a7ed64c2acedcfd6d1873f16da89896fedb5e49467f7c987c09e53ddef6d2d1ff947cbd07501173e
7
- data.tar.gz: 8724b65f9e834e5654690148a0c21444608add28c60df412dd76d47ad18f2c2edbef20ae8029de39b2431a2c6585531656e7b4a629d0427f71e717c4bd314d8b
6
+ metadata.gz: 222767562ebc5727f52d5c26b95dbfaf2de2dca0f0901e49f5ac8b35513a50052ab848fcc23f7bb69c2740789c101c5a6258de73d5651ec6a0a75e4f9c81f0f9
7
+ data.tar.gz: 93000122413fc6d307a7436a5f614c0188c6172e6d8f6330fe2a57c7bd472d1bb6e414cd9076e1d9f3a8cad3c0e1411c1a7103fcffd894f510d9aff67dad2602
data/CHANGELOG.md CHANGED
@@ -1,4 +1,26 @@
1
1
  # CHANGELOG
2
+ ## 0.4.0
3
+ * **Breaking change:** commands in extended processes syntax were renamed from start/restart/stop to ExecStart/ExecReload/ExecStop:
4
+
5
+ Was:
6
+ ```yml
7
+ processes:
8
+ web:
9
+ start: bundle exec rails server -p $PORT
10
+ restart: bundle exec pumactl phased-restart
11
+ ```
12
+
13
+ Now:
14
+ ```yml
15
+ processes:
16
+ web:
17
+ ExecStart: bundle exec rails server -p $PORT
18
+ ExecReload: bundle exec pumactl phased-restart
19
+ ```
20
+
21
+ * Added new command `exec` to run one of the defined processes (for development purposes). Example: `$ procsd exec web`.
22
+ * Added Nginx support with auto-ssl (using Certbot)
23
+
2
24
  ## 0.3.0
3
25
  * **Breaking change:** `.procsd.yml` renamed to `procsd.yml` (without dot)
4
26
  * **Breaking change:** `environment` option in the procsd.yml now has hash format, not array:
data/README.md CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  I do like the way how simple is managing of application processes in production on Heroku with [Procfile](https://devcenter.heroku.com/articles/procfile). How easily can be accessed application logs with [heroku logs](https://devcenter.heroku.com/articles/logging) command. Just type `heroku create` and you're good to go.
4
4
 
5
- Can we have something similar on the cheap Ubuntu VPS from DigitalOcean? Yes we can, all we need is a Systemd wrapper which allows to export application processes from Procfile to system services, and control them/check status/access logs using simple commands.
5
+ Can we have something similar on the cheap Ubuntu VPS from DigitalOcean? Yes we can, all we need is a **systemd wrapper** which allows to export application processes from Procfile to system services, and control them/check status/access logs using simple commands.
6
6
 
7
- > These days most of Linux distributions (including Ubuntu) has Systemd as a default system processes manager. That's why it is a good idea to use Systemd for managing application processes in production (for simple cases).
7
+ > These days most of Linux distributions (including Ubuntu) has systemd as a default system processes manager. That's why it is a good idea to use systemd for managing application processes in production (for simple cases).
8
8
 
9
9
  ## Getting started
10
10
 
11
- **Note:** latest version of Procsd is `0.3.0`. Since version `0.2.0` there are some breaking changes. Check the [CHANGELOG.md](CHANGELOG.md). Run `$ gem update procsd` or `$ bundle update procsd` (if you have already installed procsd) to update to the latest version.
11
+ > **Note:** latest version of Procsd is `0.4.0`. Since version `0.3.0` there are some breaking changes. Check the [CHANGELOG.md](CHANGELOG.md). To update to the latest version, run `$ gem update procsd` or `$ bundle update procsd` (if you have already installed procsd).
12
12
 
13
- > Install `procsd` first: `$ gem install procsd`. Required Ruby version is `>= 2.3.0`.
13
+ > **Note:** Procsd works best with Capistrano integration: [vifreefly/capistrano-procsd](https://github.com/vifreefly/capistrano-procsd)
14
+
15
+ Install `procsd` first: `$ gem install procsd`. Required Ruby version is `>= 2.3.0`.
14
16
 
15
17
  Let's say you have following application's Procfile:
16
18
 
@@ -36,23 +38,21 @@ Configuration is done.
36
38
  ### Create an application (export to Systemd)
37
39
  > To disable and remove application from Systemd there is command `$ procsd destroy`.
38
40
 
39
- > Note: `create` command needs to provide a few arguments: _--user_ (name of the current user), _--dir_ (application's working directory) and `--path` (user's $PATH). Usually it's fine to provide them like on example below:
40
-
41
41
  ```
42
42
  deploy@server:~/sample_app$ procsd create
43
43
 
44
44
  Value of the --user option: deploy
45
45
  Value of the --dir option: /home/deploy/sample_app
46
46
  Value of the --path option: /home/deploy/.rbenv/shims:/home/deploy/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
47
- Systemd directory: /etc/systemd/system
48
47
 
49
- create sample_app-web.1.service
50
- create sample_app-worker.1.service
51
- create sample_app-worker.2.service
52
- create sample_app.target
48
+ Creating app units files in the systemd directory (/etc/systemd/system)...
49
+ Create: /etc/systemd/system/sample_app-web.1.service
50
+ Create: /etc/systemd/system/sample_app-worker.1.service
51
+ Create: /etc/systemd/system/sample_app-worker.2.service
52
+ Create: /etc/systemd/system/sample_app.target
53
+ Reloaded configuraion (daemon-reload)
53
54
  Created symlink /etc/systemd/system/multi-user.target.wants/sample_app.target → /etc/systemd/system/sample_app.target.
54
55
  Enabled app target sample_app.target
55
- Reloaded configuraion (daemon-reload)
56
56
  App services were created and enabled. Run `start` to start them
57
57
 
58
58
  Note: add following line to the sudoers file (`$ sudo visudo`) if you don't want to type password each time for start/stop/restart commands:
@@ -63,8 +63,8 @@ You can provide additional options for `create` command:
63
63
  * `--user` - name of the user, default is current _$USER_ env variable
64
64
  * `--dir` - application's working directory, default is current _$PWD_ env variable
65
65
  * `--path` - $PATH to include to the each service. Default is current _$PATH_ env variable
66
- * `--add-to-sudoers` - if option present, procsd will create sudoers rule `/etc/sudoers.d/app_name` allowing to start/stop/restart app services without password prompt.
67
- * `--or-restart` - if option present and servides already created, procsd will skip creation and instead call `restart` command
66
+ * `--add-to-sudoers` - if option present, procsd will create sudoers rule file `/etc/sudoers.d/app_name` which allow to start/stop/restart app services without a password prompt (passwordless sudo).
67
+ * `--or-restart` - if option provided and services already created, procsd will skip creation and call instead `restart` command. Otherwise (if services are not present), they will be created and (in additional) started.
68
68
 
69
69
 
70
70
  ### Start application
@@ -187,6 +187,195 @@ Systemd provides [a lot of possibilities](https://www.digitalocean.com/community
187
187
  * `--priority` - Filter messages by a [particular log level.](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs#by-priority) For example show only error messages: `procsd logs --priority err`
188
188
  * `--grep` - [Filter output](https://www.freedesktop.org/software/systemd/man/journalctl.html#-g) to messages where message matches the provided query (may not work for [some](https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/1751006) Linux distributions)
189
189
 
190
+ ### Execute processes defined in Procfile
191
+
192
+ Currently, procsd can not run all processes in development like `foreman start` does. But you can run one single process using `procsd exec` command:
193
+
194
+ ```
195
+ deploy@server:~/sample_app$ PORT=3000 procsd exec web
196
+
197
+ => Booting Puma
198
+ => Rails 5.2.1 application starting in development
199
+ => Run `rails server -h` for more startup options
200
+ Puma starting in single mode...
201
+ * Version 3.12.0 (ruby 2.3.0-p0), codename: Llamas in Pajamas
202
+ * Min threads: 5, max threads: 5
203
+ * Environment: development
204
+ * Listening on tcp://localhost:3000
205
+ Use Ctrl-C to stop
206
+ ```
207
+
208
+ By default `procsd exec` skip environment variables defined in `procsd.yml`. To run process with production environment, provide `--env` option as well: `procsd exec web --env`.
209
+
210
+ ### Nginx
211
+ > Before make sure that you have Nginx installed `sudo apt install nginx` and running `sudo systemctl status nginx`.
212
+
213
+ If one of your application processes is a web process, you can automatically setup Nginx config for it. Why? For example to serve static files (assets, images, etc) directly using fast Nginx, rather than application server. Or to enable SSL support (see below).
214
+
215
+ Add to your procsd.yml `nginx` section with `server_name` option defined:
216
+
217
+ > If you don't have domain defined (or don't need it), you can add server IP instead: `server_name: 159.159.159.159`.
218
+
219
+ > If your application use multiple domains/subdomains, add all of them separated with space: `server_name: my-domain.com us.my-domain.com uk.my-domain.com`
220
+
221
+ > It's required to provide `PORT` number environment variable in `procsd.yml`. Provided port number will be used to proxy requests from Nginx to application server.
222
+
223
+ ```yml
224
+ app: sample_app
225
+ formation: web=1,worker=2
226
+ environment:
227
+ PORT: 2501
228
+ RAILS_ENV: production
229
+ RAILS_LOG_TO_STDOUT: true
230
+ nginx:
231
+ server_name: my-domain.com
232
+ ```
233
+
234
+ Configuration is done! Run [procsd create](#create-an-application-export-to-systemd) to create app services with Nginx config:
235
+
236
+ ```
237
+ deploy@server:~/sample_app$ procsd create
238
+
239
+ Creating app units files in the systemd directory (/etc/systemd/system)...
240
+ Create: /etc/systemd/system/sample_app-web.1.service
241
+ Create: /etc/systemd/system/sample_app-worker.1.service
242
+ Create: /etc/systemd/system/sample_app-worker.2.service
243
+ Create: /etc/systemd/system/sample_app.target
244
+ Reloaded configuraion (daemon-reload)
245
+ Created symlink /etc/systemd/system/multi-user.target.wants/sample_app.target → /etc/systemd/system/sample_app.target.
246
+ Enabled app target sample_app.target
247
+ App services were created and enabled. Run `start` to start them
248
+ Creating Nginx config (/etc/nginx/sites-available/sample_app)...
249
+ Create: /etc/nginx/sites-available/sample_app
250
+ Link Nginx config file to the sites-enabled folder...
251
+ Nginx config created and daemon reloaded
252
+ ```
253
+
254
+ <details/>
255
+ <summary><code>/etc/nginx/sites-available/sample_app</code>:</summary>
256
+
257
+ ```
258
+ upstream app {
259
+ server 127.0.0.1:2501;
260
+ }
261
+
262
+ server {
263
+ listen 80;
264
+ listen [::]:80;
265
+
266
+ server_name my-domain.com;
267
+ root /home/deploy/sample_app/public;
268
+
269
+ location ^~ /assets/ {
270
+ gzip_static on;
271
+ expires max;
272
+ add_header Cache-Control public;
273
+ }
274
+
275
+ try_files $uri/index.html $uri @app;
276
+ location @app {
277
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
278
+ proxy_set_header Host $http_host;
279
+ proxy_set_header X-Forwarded-Proto $scheme;
280
+ proxy_set_header X-Real-IP $remote_addr;
281
+ proxy_redirect off;
282
+ proxy_pass http://app;
283
+ }
284
+
285
+ client_max_body_size 256M;
286
+ keepalive_timeout 60;
287
+ error_page 500 502 503 504 /500.html;
288
+ error_page 404 /404.html;
289
+ error_page 422 /422.html;
290
+ }
291
+ ```
292
+ </details>
293
+
294
+ #### Auto SSL using Certbot
295
+
296
+ To generate Nginx config with free SSL certificate (from [Let’s Encrypt](https://letsencrypt.org/)) included, you need to install [Certbot](https://certbot.eff.org/) on the remote server first:
297
+
298
+ ```
299
+ sudo add-apt-repository ppa:certbot/certbot
300
+ sudo apt update
301
+ sudo apt-get install python-certbot-nginx
302
+ ```
303
+
304
+ Then update procsd.yml:
305
+
306
+ > It's required to provide contact email to obtain free certificate from Let’s Encrypt
307
+
308
+ ```yml
309
+ nginx:
310
+ server_name: my-domain.com
311
+ certbot:
312
+ email: my-contact-email@gmail.com
313
+ ```
314
+
315
+ Configuration is done. **Make sure that all domains defined in procsd (nginx.server_name) are pointed to server IP** where the application is hosted. Now run `procsd create` as usual:
316
+
317
+ <details/>
318
+ <summary>Output</summary>
319
+
320
+ ```
321
+ deploy@server:~/sample_app$ procsd create
322
+
323
+ Creating app units files in the systemd directory (/etc/systemd/system)...
324
+ Create: /etc/systemd/system/sample_app-web.1.service
325
+ Create: /etc/systemd/system/sample_app-worker.1.service
326
+ Create: /etc/systemd/system/sample_app-worker.2.service
327
+ Create: /etc/systemd/system/sample_app.target
328
+ Reloaded configuraion (daemon-reload)
329
+ Created symlink /etc/systemd/system/multi-user.target.wants/sample_app.target → /etc/systemd/system/sample_app.target.
330
+ Enabled app target sample_app.target
331
+ App services were created and enabled. Run `start` to start them
332
+ Creating Nginx config (/etc/nginx/sites-available/sample_app)...
333
+ Create: /etc/nginx/sites-available/sample_app
334
+ Link Nginx config file to the sites-enabled folder...
335
+ Nginx config created and daemon reloaded
336
+
337
+ Execute: sudo certbot --agree-tos --no-eff-email --non-interactive --nginx -d my-domain.com -m my-contact-email@gmail.com
338
+ Saving debug log to /var/log/letsencrypt/letsencrypt.log
339
+ Plugins selected: Authenticator nginx, Installer nginx
340
+ Obtaining a new certificate
341
+ Performing the following challenges:
342
+ http-01 challenge for my-domain.com
343
+ Waiting for verification...
344
+ Cleaning up challenges
345
+ Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/sample_app
346
+ Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/sample_app
347
+
348
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
349
+ Congratulations! You have successfully enabled https://my-domain.com
350
+
351
+ You should test your configuration at:
352
+ https://www.ssllabs.com/ssltest/analyze.html?d=my-domain.com
353
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
354
+
355
+ IMPORTANT NOTES:
356
+ - Congratulations! Your certificate and chain have been saved at:
357
+ /etc/letsencrypt/live/my-domain.com/fullchain.pem
358
+ Your key file has been saved at:
359
+ /etc/letsencrypt/live/my-domain.com/privkey.pem
360
+ Your cert will expire on 2019-02-17. To obtain a new or tweaked
361
+ version of this certificate in the future, simply run certbot again
362
+ with the "certonly" option. To non-interactively renew *all* of
363
+ your certificates, run "certbot renew"
364
+ - Your account credentials have been saved in your Certbot
365
+ configuration directory at /etc/letsencrypt. You should make a
366
+ secure backup of this folder now. This configuration directory will
367
+ also contain certificates and private keys obtained by Certbot so
368
+ making regular backups of this folder is ideal.
369
+ - If you like Certbot, please consider supporting our work by:
370
+
371
+ Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
372
+ Donating to EFF: https://eff.org/donate-le
373
+
374
+ Successfully installed SSL cert using certbot
375
+ ```
376
+ </details>
377
+
378
+
190
379
  ## All available commands
191
380
 
192
381
  ```
@@ -194,11 +383,12 @@ $ procsd --help
194
383
 
195
384
  Commands:
196
385
  procsd --version, -v # Print the version
197
- procsd config # Show configuration. Available types: sudoers
386
+ procsd config # Print config files based on current settings. Available types: sudoers
198
387
  procsd create # Create and enable app services
199
388
  procsd destroy # Stop, disable and remove app services
200
389
  procsd disable # Disable app target
201
390
  procsd enable # Enable app target
391
+ procsd exec # Run app process
202
392
  procsd help [COMMAND] # Describe available commands or one specific command
203
393
  procsd list # List all app services
204
394
  procsd logs # Show app services logs
@@ -213,11 +403,13 @@ Commands:
213
403
 
214
404
  [Foreman](http://ddollar.github.io/foreman/) itself designed for _development_ (not production) usage only and doing it great. Yes, Foreman allows to [export](http://ddollar.github.io/foreman/#EXPORTING) Procfile to the Systemd, but that's all. After export you have to manually use `systemctl` and `journalctl` to manage/check exported services. Procsd not only exports application, but provides [simple commands](#all-available-commands) to manage exported target.
215
405
 
216
- There is another difference in the export logic. Foreman systemd export uses [dymamic](https://fedoramagazine.org/systemd-template-unit-files/) services templates and as a result generates a lot of files/folders in the systemd directory even for a simple application.
406
+ * Foreman systemd export uses [dymamic](https://fedoramagazine.org/systemd-template-unit-files/) services templates and as a result generates quite a lot of files/folders in the systemd directory even for a simple application.
217
407
 
218
- Services names contains [$PORT variable](http://ddollar.github.io/foreman/#PROCFILE) at the end. If Procfile has multiple processes, each exported service will have following naming format: `<app_name>-<process_name>@<port_varible_number += 100>.service` (and it's [undocumented](http://ddollar.github.io/foreman/#SYSTEMD-EXPORT) logic). For example for Procfile and formation (_web=1, worker=2_) above, exported services with Foreman will be: `sample_app-web@2500.service`, `sample_app-worker@2600.service` and `sample_app-worker@2601.service`. My opinion about this approach: it's complicated. Why is there PORT variable, which is also incremented for each service? How I'm supposed to remember all these services names to manage or check services status/logs (it's required to provide full names of services for systemctl/journalctl)?
408
+ * Services generated using Foreman contain [$PORT variable](http://ddollar.github.io/foreman/#PROCFILE) in their names (and it's [undocumented](http://ddollar.github.io/foreman/#SYSTEMD-EXPORT) logic). For example for Procfile and formation `web=1,worker=2` (from example above), exported services with Foreman will be: `sample_app-web@2500.service`, `sample_app-worker@2600.service` and `sample_app-worker@2601.service`. My opinion about this approach: it's complicated. Why is there required PORT variable in the services names? Procsd following one rule: simplicity. For export it uses static service files (that means for each process will be generated it's own service file) and services names have predictable, Heroku-like names.
219
409
 
220
- Procsd following one rule: simplicity. For export it uses static service files (that means for each process will be generated it's own service file) and services names have predictable, Heroku-like names.
410
+ * Procsd export can provide additional stop/restart commands for each service (see _Notes_ below).
411
+
412
+ * To delete existing app services from Systemd, there is `procsd destroy` command. It is doing the following: stop services if they are running, delete all required systemd files from systemd directory, and restart systemd (`daemon-reload`). This command especially useful while testing, when you need frequently create/update configuration.
221
413
 
222
414
 
223
415
  ## Notes
@@ -228,32 +420,36 @@ Procsd following one rule: simplicity. For export it uses static service files (
228
420
  ```
229
421
  deploy@server:~/sample_app$ VERBOSE=true procsd logs -n 3
230
422
 
231
- > Executing command: `journalctl --no-pager --no-hostname --all --output short-iso -n 3 --unit sample_app-*`
423
+ Execute: journalctl --no-pager --no-hostname --all --output short-iso -n 3 --unit sample_app-*
232
424
 
233
425
  -- Logs begin at Sun 2018-10-21 00:38:42 +04, end at Sun 2018-11-04 19:17:01 +04. --
234
426
  2018-11-04T19:11:59+0400 sample_app-worker.2[29907]: 2018-11-04T15:11:59.597Z 29907 TID-gne5aeyuz INFO: Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org
235
427
  2018-11-04T19:11:59+0400 sample_app-worker.2[29907]: 2018-11-04T15:11:59.597Z 29907 TID-gne5aeyuz INFO: Booting Sidekiq 5.2.2 with redis options {:id=>"Sidekiq-server-PID-29907", :url=>nil}
236
428
  2018-11-04T19:11:59+0400 sample_app-worker.2[29907]: 2018-11-04T15:11:59.601Z 29907 TID-gne5aeyuz INFO: Starting processing, hit Ctrl-C to stop
237
429
  ```
238
- * You can use extended format of processes commands inside `procsd.yml` to provide additional restart/stop commands for processes:
430
+ * You can use extended format of processes commands inside `procsd.yml` to provide additional restart/stop commands for each process:
431
+
432
+ > All possible options: `ExecStart`, `ExecReload` and `ExecStop`
239
433
 
240
- > All possible options: `start`, `restart` and `stop`
241
434
  > If procsd.yml has `processes:` option defined, then content of Procfile will be ignored
242
435
 
243
436
  ```yml
437
+ app: sample_app
244
438
  processes:
245
439
  web:
246
- start: bundle exec rails server -p $PORT
247
- restart: bundle exec pumactl phased-restart
440
+ ExecStart: bundle exec rails server -p $PORT
441
+ ExecReload: bundle exec pumactl phased-restart
248
442
  worker: bundle exec sidekiq -e production
249
- app: sample_app
250
443
  ```
251
444
 
252
- Why? For example default Ruby on Rails application server [Puma](http://puma.io/) supports [Phased or Rolling restart](https://github.com/puma/puma/blob/master/docs/restart.md#normal-vs-hot-vs-phased-restart) feature. If you provide separate `restart`command for a process, then this command will be called (`$ procsd restart`) by Systemd instead of just killing and starting process again.
445
+ Why? For example default Ruby on Rails application server [Puma](http://puma.io/) supports [Phased or Rolling restart](https://github.com/puma/puma/blob/master/docs/restart.md#normal-vs-hot-vs-phased-restart) feature. If you provide separate `ExecReload`command for a process, then this command will be called while executing `$ procsd restart` by systemd instead of just killing and starting process again.
446
+
447
+ ## Capistrano integration
448
+
449
+ https://github.com/vifreefly/capistrano-procsd
253
450
 
254
451
 
255
452
  ## ToDo
256
- * Optional possibility to generate Ngnix config (with out-of-box SSL using [Certbot](https://certbot.eff.org/lets-encrypt/ubuntubionic-nginx)) for an application to use Ngnix as a proxy and serve static files
257
453
  * Add integration with [Inspeqtor](https://github.com/mperham/inspeqtor) to monitor application services and get alert notifications if something happened
258
454
 
259
455
 
data/lib/procsd/cli.rb CHANGED
@@ -1,71 +1,45 @@
1
1
  require 'yaml'
2
- require 'erb'
3
2
  require_relative 'generator'
4
3
 
5
4
  module Procsd
6
5
  class CLI < Thor
7
6
  class ConfigurationError < StandardError; end
8
7
  class ArgumentError < StandardError; end
9
- map %w[--version -v] => :__print_version
10
8
 
11
9
  desc "create", "Create and enable app services"
12
- option :user, aliases: :u, type: :string, banner: "$USER"
13
- option :dir, aliases: :d, type: :string, banner: "$PWD"
14
- option :path, aliases: :p, type: :string, banner: "$PATH"
10
+ option :user, aliases: :u, type: :string, banner: "$USER", default: ENV["USER"]
11
+ option :dir, aliases: :d, type: :string, banner: "$PWD", default: ENV["PWD"]
12
+ option :path, aliases: :p, type: :string, banner: "$PATH", default: `/bin/bash -ilc 'echo $PATH'`.strip
15
13
  option :'or-restart', type: :boolean, banner: "Create and start app services if not created yet, otherwise restart"
16
14
  option :'add-to-sudoers', type: :boolean, banner: "Create sudoers rule at /etc/sudoers.d/app_name to allow manage app target without password prompt"
17
15
  def create
18
- preload!
19
-
20
- if !target_exist?
21
- opts = {
22
- user: options["user"] || ENV["USER"],
23
- dir: options["dir"] || ENV["PWD"],
24
- path: options["path"] || fetch_path_env
25
- }
26
-
27
- opts.each do |key, value|
28
- if value.nil? || value.empty?
29
- say("Can't fetch value for --#{key}, please provide it as an argument", :red) and return
30
- else
31
- say "Value of the --#{key} option: #{value}"
32
- end
16
+ raise ConfigurationError, "Can't find systemctl executable available" unless in_path?("systemctl")
17
+ options.each do |key, value|
18
+ next unless %w(user dir path).include? key
19
+ if value.nil? || value.empty?
20
+ say("Can't fetch value for --#{key}, please provide it's as an argument", :red) and return
21
+ else
22
+ say("Value of the --#{key} option: #{value}", :yellow)
33
23
  end
24
+ end
34
25
 
35
- gen = Generator.new
36
- gen.export!(services, config: @config, options: options.merge(opts))
37
-
38
- enable
39
- if execute %w(sudo systemctl daemon-reload)
40
- say("Reloaded configuraion (daemon-reload)", :green)
26
+ preload!
27
+ if @config[:nginx]
28
+ raise ConfigurationError, "Can't find nginx executable available" unless in_path?("nginx")
29
+ unless Dir.exist?(File.join options["dir"], "public")
30
+ raise ConfigurationError, "Missing public/ folder to use with Nginx"
41
31
  end
42
-
43
- if options["or-restart"]
44
- start
45
- say("App services were created, enabled and started", :green)
46
- else
47
- say("App services were created and enabled. Run `start` to start them", :green)
32
+ unless @config.dig(:environment, "PORT")
33
+ raise ConfigurationError, "Please provide PORT environment variable in procsd.yml to use with Nginx"
48
34
  end
49
-
50
- sudoers_rule_content = generate_sudoers_rule(opts[:user])
51
- if options["add-to-sudoers"]
52
- sudoers_file_temp_path = "/tmp/#{app_name}"
53
- sudoers_file_dest_path = "#{SUDOERS_DIR}/#{app_name}"
54
- if Dir.exist?(SUDOERS_DIR)
55
- File.open(sudoers_file_temp_path, "w") { |f| f.puts sudoers_rule_content }
56
- execute %W(sudo chown root:root #{sudoers_file_temp_path})
57
- execute %W(sudo chmod 0440 #{sudoers_file_temp_path})
58
- if execute %W(sudo mv #{sudoers_file_temp_path} #{sudoers_file_dest_path})
59
- say("Sudoers file #{sudoers_file_dest_path} was created", :green)
60
- end
61
- else
62
- say "Directory #{SUDOERS_DIR} does not exist, sudoers file wan't created"
63
- end
64
- else
65
- say "Note: add following line to the sudoers file (`$ sudo visudo`) if you don't " \
66
- "want to type password each time for start/stop/restart commands:"
67
- puts sudoers_rule_content
35
+ if certbot = @config[:nginx]["certbot"]
36
+ raise ConfigurationError, "Can't find certbot executable available" unless in_path?("certbot")
37
+ raise ConfigurationError, "Provide email to generate cert using certbot" unless certbot["email"]
68
38
  end
39
+ end
40
+
41
+ if !target_exist?
42
+ perform_create
69
43
  else
70
44
  if options["or-restart"]
71
45
  restart
@@ -83,9 +57,9 @@ module Procsd
83
57
  stop
84
58
  disable
85
59
 
86
- services.keys.push(target_name).each do |filename|
60
+ units.each do |filename|
87
61
  path = File.join(systemd_dir, filename)
88
- execute %W(sudo rm #{path}) and say "Deleted #{path}" if File.exist? path
62
+ execute %W(sudo rm #{path}) and say "Deleted: #{path}" if File.exist?(path)
89
63
  end
90
64
 
91
65
  if execute %w(sudo systemctl daemon-reload)
@@ -94,10 +68,19 @@ module Procsd
94
68
  say("App services were stopped, disabled and removed", :green)
95
69
 
96
70
  sudoers_file_path = "#{SUDOERS_DIR}/#{app_name}"
97
- if File.exist?(sudoers_file_path)
98
- if yes?("Remove sudoers rule #{sudoers_file_path} ? (yes/no)")
99
- say("Sudoers file removed", :green) if execute %W(sudo rm #{sudoers_file_path})
71
+ if system "sudo", "test", "-e", sudoers_file_path
72
+ say("Sudoers file removed", :green) if execute %W(sudo rm #{sudoers_file_path})
73
+ end
74
+
75
+ if @config[:nginx]
76
+ enabled_path = File.join(NGINX_DIR, "sites-enabled", app_name)
77
+ available_path = File.join(NGINX_DIR, "sites-available", app_name)
78
+ [enabled_path, available_path].each do |path|
79
+ execute %W(sudo rm #{path}) and say "Deleted: #{path}" if File.exist?(path)
100
80
  end
81
+
82
+ execute %w(sudo systemctl restart nginx)
83
+ say("Nginx config removed and daemon reloaded", :green)
101
84
  end
102
85
  else
103
86
  say_target_not_exists
@@ -182,7 +165,7 @@ module Procsd
182
165
  end
183
166
 
184
167
  command << (options["target"] ? target_name : "#{app_name}-#{service_name}*")
185
- execute command
168
+ execute command, type: :exec
186
169
  end
187
170
 
188
171
  desc "logs", "Show app services logs"
@@ -202,7 +185,7 @@ module Procsd
202
185
  command.push("--grep", "'" + options["grep"] + "'") if options["grep"]
203
186
 
204
187
  command.push("--unit", "#{app_name}-#{service_name}*")
205
- execute command
188
+ execute command, type: :exec
206
189
  end
207
190
 
208
191
  desc "list", "List all app services"
@@ -210,15 +193,11 @@ module Procsd
210
193
  preload!
211
194
  say_target_not_exists and return unless target_exist?
212
195
 
213
- execute %W(systemctl list-dependencies #{target_name})
196
+ command = %W(systemctl list-dependencies #{target_name})
197
+ execute command, type: :exec
214
198
  end
215
199
 
216
- desc "--version, -v", "Print the version"
217
- def __print_version
218
- puts VERSION
219
- end
220
-
221
- desc "config", "Show configuration. Available types: sudoers"
200
+ desc "config", "Print config files based on current settings. Available types: sudoers"
222
201
  def config(name)
223
202
  preload!
224
203
 
@@ -230,32 +209,109 @@ module Procsd
230
209
  end
231
210
  end
232
211
 
212
+ map exec: :__exec
213
+ desc "exec", "Run app process"
214
+ option :env, type: :boolean, banner: "Require environment defined in procsd.yml"
215
+ def __exec(process_name)
216
+ preload!
217
+
218
+ start_cmd = @config[:processes].dig(process_name, "commands", "ExecStart")
219
+ raise ArgumentError, "Process is not defined: #{process_name}" unless start_cmd
220
+
221
+ if options["env"]
222
+ @config[:environment].each { |k, v| @config[:environment][k] = v.to_s }
223
+ exec @config[:environment], start_cmd
224
+ else
225
+ exec start_cmd
226
+ end
227
+ end
228
+
229
+ map %w[--version -v] => :__print_version
230
+ desc "--version, -v", "Print the version"
231
+ def __print_version
232
+ puts VERSION
233
+ end
234
+
233
235
  private
234
236
 
235
- def generate_sudoers_rule(user)
236
- commands = []
237
- systemctl_path = `which systemctl`.strip
237
+ def perform_create
238
+ generator = Generator.new(@config, options)
239
+ generator.generate_units(save: true)
240
+
241
+ if execute %w(sudo systemctl daemon-reload)
242
+ say("Reloaded configuraion (daemon-reload)", :green)
243
+ end
244
+
245
+ enable
246
+
247
+ if options["or-restart"]
248
+ start
249
+ say("App services were created, enabled and started", :green)
250
+ else
251
+ say("App services were created and enabled. Run `start` to start them", :green)
252
+ end
253
+
254
+ if options["add-to-sudoers"]
255
+ if Dir.exist?(SUDOERS_DIR)
256
+ if generator.generate_sudoers(options["user"], has_reload: has_reload?, save: true)
257
+ say("Sudoers file #{SUDOERS_DIR}/#{app_name} was created", :green)
258
+ end
259
+ else
260
+ say("Directory #{SUDOERS_DIR} does not exists, sudoers file wasn't created", :red)
261
+ end
262
+ else
263
+ say "Note: add following line to the sudoers file (`$ sudo visudo`) if you don't " \
264
+ "want to type password each time for start/stop/restart commands:"
265
+ puts generator.generate_sudoers(options["user"], has_reload: has_reload?)
266
+ end
267
+
268
+ if nginx = @config[:nginx]
269
+ generator.generate_nginx_conf(save: true)
270
+ execute %w(sudo systemctl restart nginx)
271
+ say("Nginx config created and daemon reloaded", :green)
272
+
273
+ # Reference: https://certbot.eff.org/docs/using.html#certbot-command-line-options
274
+ if certbot = nginx["certbot"]
275
+ command = %w(sudo certbot --agree-tos --no-eff-email --redirect --non-interactive --nginx)
276
+ nginx["server_name"].split(" ").map(&:strip).each do |domain|
277
+ command.push("-d", domain)
278
+ end
238
279
 
239
- %w(start stop restart).each { |cmd| commands << "#{systemctl_path} #{cmd} #{target_name}" }
240
- commands << "#{systemctl_path} reload-or-restart #{app_name}-\\* --all" if has_reload?
280
+ command.push("-m", certbot["email"])
281
+ if execute command
282
+ say("Successfully installed SSL cert using certbot", :green)
283
+ else
284
+ say("Failed to install SSL cert using certbot", :red)
285
+ end
286
+ end
287
+ end
288
+ end
241
289
 
242
- "#{user} ALL=NOPASSWD: #{commands.join(', ')}"
290
+ def in_path?(name)
291
+ system("which", name, [:out, :err] => "/dev/null")
243
292
  end
244
293
 
245
294
  def has_reload?
246
- services.any? { |_, opts| opts["restart"] }
295
+ @config[:processes].any? { |name, values| values.dig("commands", "ExecReload") }
247
296
  end
248
297
 
249
- def fetch_path_env
250
- # get value of the $PATH env variable including ~/.bashrc as well (-i flag)
251
- `/bin/bash -ilc 'echo $PATH'`.strip
252
- end
298
+ def units
299
+ all = [target_name]
300
+ @config[:processes].each do |name, values|
301
+ values["size"].times { |i| all << "#{app_name}-#{name}.#{i + 1}.service" }
302
+ end
253
303
 
254
- def execute(command)
255
- trap("INT") { puts "\nInterrupted" ; exit 130 }
304
+ all
305
+ end
256
306
 
257
- say("> Executing command: `#{command.join(' ')}`", :yellow) if ENV["VERBOSE"] == "true"
258
- system *command
307
+ def execute(command, type: :system)
308
+ say("Execute: #{command.join(' ')}", :yellow) if ENV["VERBOSE"] == "true"
309
+ case type
310
+ when :system
311
+ system *command
312
+ when :exec
313
+ exec *command
314
+ end
259
315
  end
260
316
 
261
317
  def say_target_not_exists
@@ -286,24 +342,9 @@ module Procsd
286
342
  @config[:app]
287
343
  end
288
344
 
289
- def services
290
- all = {}
291
- @config[:processes].each do |process_name, opts|
292
- opts["count"].times do |i|
293
- commands = { "start" => opts["start"], "stop" => opts["stop"], "restart" => opts["restart"] }
294
- all["#{app_name}-#{process_name}.#{i + 1}.service"] = commands
295
- end
296
- end
297
-
298
- all
299
- end
300
-
301
345
  def preload!
302
- @config = {}
346
+ @config = { processes: {}}
303
347
 
304
- unless system("which", "systemctl", [:out, :err]=>"/dev/null")
305
- raise ConfigurationError, "Your OS doesn't has systemctl executable available"
306
- end
307
348
  raise ConfigurationError, "Config file procsd.yml doesn't exists" unless File.exist? "procsd.yml"
308
349
  begin
309
350
  procsd = YAML.load(ERB.new(File.read "procsd.yml").result)
@@ -335,19 +376,18 @@ module Procsd
335
376
  processes = procsd["processes"] || procfile
336
377
  processes.each do |process_name, opts|
337
378
  if opts.kind_of?(Hash)
338
- raise ConfigurationError, "Missing start command for `#{process_name}` process" unless opts["start"]
379
+ raise ConfigurationError, "Missing ExecStart command for `#{process_name}` process" unless opts["ExecStart"]
380
+ @config[:processes][process_name] = { "commands" => opts }
339
381
  else
340
- processes[process_name] = { "start" => opts }
382
+ @config[:processes][process_name] = { "commands" => { "ExecStart" => opts }}
341
383
  end
342
384
 
343
- unless processes[process_name]["count"]
344
- processes[process_name]["count"] = formation[process_name] || 1
345
- end
385
+ @config[:processes][process_name]["size"] = formation[process_name] || 1
346
386
  end
347
387
 
348
- @config[:processes] = processes
349
388
  @config[:environment] = procsd["environment"] || {}
350
389
  @config[:systemd_dir] = procsd["systemd_dir"] || DEFAULT_SYSTEMD_DIR
390
+ @config[:nginx] = procsd["nginx"]
351
391
  end
352
392
  end
353
393
  end
@@ -1,47 +1,110 @@
1
1
  module Procsd
2
- class Generator < Thor::Group
3
- include Thor::Actions
2
+ class Generator
3
+ attr_reader :app_name, :target_name
4
4
 
5
- def self.source_root
6
- File.dirname(__FILE__)
7
- end
8
-
9
- def export!(services, config:, options:)
10
- self.destination_root = "/tmp"
5
+ def initialize(config, options)
11
6
  @config = config
12
- say "Systemd directory: #{@config[:systemd_dir]}"
13
-
14
- app_name = @config[:app]
15
- target_name = "#{app_name}.target"
7
+ @options = options
8
+ @app_name = @config[:app]
9
+ @target_name = "#{app_name}.target"
10
+ end
16
11
 
17
- services.each do |service_name, service_command|
18
- service_config = options.merge(
12
+ def generate_units(save: false)
13
+ services = {}
14
+ @config[:processes].each do |name, values|
15
+ commands = values["commands"]
16
+ size = values["size"]
17
+ content = generate_template("service", @options.merge(
19
18
  "target_name" => target_name,
20
- "id" => service_name.sub(".service", ""),
21
- "command" => service_command,
19
+ "commands" => commands,
22
20
  "environment" => @config[:environment]
23
- )
21
+ ))
24
22
 
25
- generate(service_name, service_config, type: :service)
23
+ services[name] = { content: content, size: size }
26
24
  end
27
25
 
28
- target_config = {
29
- "app" => app_name,
30
- "services" => services.keys
31
- }
32
- generate(target_name, target_config, type: :target)
26
+ if save
27
+ puts "Creating app units files in the systemd directory (#{DEFAULT_SYSTEMD_DIR})..."
28
+ wants = []
29
+ services.each do |service_name, values|
30
+ values[:size].times do |i|
31
+ unit_name = "#{app_name}-#{service_name}.#{i + 1}.service"
32
+ wants << unit_name
33
+ write_file!(File.join(@config[:systemd_dir], unit_name), values[:content])
34
+ end
35
+ end
36
+
37
+ target_content = generate_template("target", {
38
+ "app" => app_name,
39
+ "wants" => wants.join(" ")
40
+ })
41
+ write_file!(File.join(@config[:systemd_dir], target_name), target_content)
42
+ else
43
+ services
44
+ end
45
+ end
46
+
47
+ def generate_sudoers(user, has_reload:, save: false)
48
+ systemctl_path = `which systemctl`.strip
49
+ commands = []
50
+ %w(start stop restart).each { |cmd| commands << "#{systemctl_path} #{cmd} #{target_name}" }
51
+ commands << "#{systemctl_path} reload-or-restart #{app_name}-\\* --all" if has_reload
52
+ content = "#{user} ALL=NOPASSWD: #{commands.join(', ')}"
53
+
54
+ if save
55
+ puts "Creating sudoers rule file in the sudoers.d directory (#{SUDOERS_DIR})..."
56
+ temp_path = "/tmp/#{app_name}"
57
+ dest_path = "#{SUDOERS_DIR}/#{app_name}"
58
+
59
+ File.open(temp_path, "w") { |f| f.puts content }
60
+ system "sudo", "chown", "root:root", temp_path
61
+ system "sudo", "chmod", "0440", temp_path
62
+ system "sudo", "mv", temp_path, dest_path
63
+ else
64
+ content
65
+ end
66
+ end
67
+
68
+ def generate_nginx_conf(save: false)
69
+ root_path = File.join(@options["dir"], "public")
70
+ content = generate_template("nginx", {
71
+ port: @config[:environment]["PORT"],
72
+ server_name: @config[:nginx]["server_name"],
73
+ root: root_path,
74
+ error_500: File.exist?(File.join root_path, "500.html"),
75
+ error_404: File.exist?(File.join root_path, "404.html"),
76
+ error_422: File.exist?(File.join root_path, "422.html")
77
+ })
78
+
79
+ if save
80
+ config_path = File.join(NGINX_DIR, "sites-available", app_name)
81
+ puts "Creating Nginx config (#{config_path})..."
82
+ write_file!(config_path, content)
83
+ puts "Link Nginx config file to the sites-enabled folder..."
84
+ system "sudo", "ln", "-nfs", config_path, File.join(NGINX_DIR, "sites-enabled")
85
+ else
86
+ content
87
+ end
33
88
  end
34
89
 
35
90
  private
36
91
 
37
- def generate(filename, conf, type:)
38
- template("templates/#{type}.erb", filename, conf)
92
+ def generate_template(template_name, conf)
93
+ b = binding
94
+ b.local_variable_set(:config, conf)
95
+ template_path = File.join(File.dirname(__FILE__), "templates/#{template_name}.erb")
96
+ content = File.read(template_path)
97
+ ERB.new(content, nil, "-").result(b)
98
+ end
39
99
 
40
- source_path = File.join(destination_root, filename)
41
- dest_path = File.join(@config[:systemd_dir], filename)
42
- system "sudo", "mv", source_path, dest_path
100
+ def write_file!(dest_path, content)
101
+ temp_path = File.join("/tmp", Pathname.new(dest_path).basename.to_s)
102
+ File.write(temp_path, content)
103
+ if system "sudo", "mv", temp_path, dest_path
104
+ puts "Create: #{dest_path}"
105
+ end
43
106
  ensure
44
- File.delete(source_path) if File.exist? source_path
107
+ File.delete(temp_path) if File.exist? temp_path
45
108
  end
46
109
  end
47
110
  end
@@ -0,0 +1,39 @@
1
+ upstream app {
2
+ server 127.0.0.1:<%= config[:port] %>;
3
+ }
4
+
5
+ server {
6
+ listen 80;
7
+ listen [::]:80;
8
+
9
+ server_name <%= config[:server_name] %>;
10
+ root <%= config[:root] %>;
11
+
12
+ location ^~ /assets/ {
13
+ gzip_static on;
14
+ expires max;
15
+ add_header Cache-Control public;
16
+ }
17
+
18
+ try_files $uri/index.html $uri @app;
19
+ location @app {
20
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
21
+ proxy_set_header Host $http_host;
22
+ proxy_set_header X-Forwarded-Proto $scheme;
23
+ proxy_set_header X-Real-IP $remote_addr;
24
+ proxy_redirect off;
25
+ proxy_pass http://app;
26
+ }
27
+
28
+ client_max_body_size 256M;
29
+ keepalive_timeout 60;
30
+ <% if config[:error_500] -%>
31
+ error_page 500 502 503 504 /500.html;
32
+ <% end -%>
33
+ <% if config[:error_404] -%>
34
+ error_page 404 /404.html;
35
+ <% end -%>
36
+ <% if config[:error_422] -%>
37
+ error_page 422 /422.html;
38
+ <% end -%>
39
+ }
@@ -1,7 +1,7 @@
1
1
  [Unit]
2
2
  Requires=network.target
3
3
  PartOf=<%= config["target_name"] %>
4
- <% if config["command"]["restart"] -%>
4
+ <% if config["commands"]["ExecReload"] -%>
5
5
  ReloadPropagatedFrom=<%= config["target_name"] %>
6
6
  <% end -%>
7
7
 
@@ -10,20 +10,20 @@ Type=simple
10
10
  User=<%= config["user"] %>
11
11
  WorkingDirectory=<%= config["dir"] %>
12
12
 
13
- ExecStart=/bin/bash -lc '<%= config["command"]["start"] %>'
14
- <% if stop = config["command"]["stop"] -%>
13
+ ExecStart=/bin/bash -lc '<%= config["commands"]["ExecStart"] %>'
14
+ <% if stop = config["commands"]["ExecStop"] -%>
15
15
  ExecStop=/bin/bash -lc '<%= stop %>'
16
16
  <% end -%>
17
- <% if config["command"]["restart"] -%>
18
- ExecReload=/bin/bash -lc '<%= config["command"]["restart"] %>'
17
+ <% if reload = config["commands"]["ExecReload"] -%>
18
+ ExecReload=/bin/bash -lc '<%= reload %>'
19
19
  <% end -%>
20
20
 
21
21
  Restart=always
22
22
  RestartSec=1
23
- TimeoutStopSec=15
23
+ TimeoutStopSec=30
24
24
  KillMode=mixed
25
25
  StandardInput=null
26
- SyslogIdentifier=<%= config["id"] %>
26
+ SyslogIdentifier=%n
27
27
 
28
28
  Environment="PATH=<%= config["path"] %>"
29
29
  <% config["environment"].each do |key, value| -%>
@@ -1,5 +1,5 @@
1
1
  [Unit]
2
- Wants=<%= config["services"].join(" ") %>
2
+ Wants=<%= config["wants"] %>
3
3
 
4
4
  [Install]
5
5
  WantedBy=multi-user.target
@@ -1,3 +1,3 @@
1
1
  module Procsd
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/procsd.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'dotenv/load'
2
2
  require 'thor'
3
+ require 'erb'
3
4
  require 'procsd/version'
4
5
 
5
6
  module Procsd
6
7
  DEFAULT_SYSTEMD_DIR = "/etc/systemd/system".freeze
7
8
  SUDOERS_DIR = "/etc/sudoers.d".freeze
9
+ NGINX_DIR = "/etc/nginx".freeze
8
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Afanasev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-08 00:00:00.000000000 Z
11
+ date: 2018-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -100,6 +100,7 @@ files:
100
100
  - lib/procsd.rb
101
101
  - lib/procsd/cli.rb
102
102
  - lib/procsd/generator.rb
103
+ - lib/procsd/templates/nginx.erb
103
104
  - lib/procsd/templates/service.erb
104
105
  - lib/procsd/templates/target.erb
105
106
  - lib/procsd/version.rb