devup 0.2.0 → 0.3.3

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: 16b7a8648ef05f7206cc02900450f8f303fc948a0976ac959472451eeef1988f
4
- data.tar.gz: a58834288dcd48d1cde4e353f1a77fb40b15419751896bdd7680b71be64a7266
3
+ metadata.gz: d83b3c98d30a71d2414a1fc5106d9a907612911f4b05a2246404f08215be203c
4
+ data.tar.gz: 772cee331bdcd822f497cc7121a6d6201b971b65b930e47864770d7cbb30df62
5
5
  SHA512:
6
- metadata.gz: 340f23ba29e0cd7e9c187f3ddb20d5e7c825503c5095fd4e3acd244ae2f714f89c2cfe636533207391292685244f4459b70af87a09ad82e7e8b3d9e70639aead
7
- data.tar.gz: d596434a6a8450bf01b433a2f2e4c3abbed3f465046463954595badb82468fabd0f3fc6479d0fe9935dc1bbd48b246e11aabd723251b81b2d288ff7d103b822c
6
+ metadata.gz: e29107cb8acd259d0e1f038e93cd7809e5b933abdedace59e17001e974220632ed95fe14aadb8e28007c6164665fbd4b4398268b1b476d35042c89457dde78b7
7
+ data.tar.gz: 7145ada30c07f14547ea5fc4c3d6ef9222a7dee288bb2d38b547a1907d844646e737d43a8f740668739465de60217d7e1a0501601618bb5e270c77e99903e713
data/.gitignore CHANGED
@@ -14,6 +14,8 @@
14
14
  # Editor config
15
15
  .vimrc
16
16
 
17
+ .env.services
17
18
  spec/dummy/.env.services
19
+ spec/dummy_rails/.env.services
18
20
 
19
21
  devup-*.gem
@@ -0,0 +1,2 @@
1
+ ignore:
2
+ - 'spec/dummy_rails/**/*'
@@ -5,6 +5,8 @@ language: ruby
5
5
  cache: bundler
6
6
  rvm:
7
7
  - 2.7.1
8
+ - 2.6.6
9
+ - 2.5.8
8
10
  services:
9
11
  - docker
10
12
  before_install:
data/Gemfile CHANGED
@@ -8,3 +8,4 @@ gem "rspec", "~> 3.0"
8
8
  gem "standard"
9
9
  gem "simplecov", require: false
10
10
  gem "byebug"
11
+ gem "lefthook"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- devup (0.2.0)
4
+ devup (0.3.3)
5
5
  dotenv
6
6
 
7
7
  GEM
@@ -14,6 +14,7 @@ GEM
14
14
  dotenv (2.7.5)
15
15
  jaro_winkler (1.5.4)
16
16
  json (2.3.0)
17
+ lefthook (0.7.2)
17
18
  parallel (1.19.1)
18
19
  parser (2.7.1.0)
19
20
  ast (~> 2.4.0)
@@ -60,6 +61,7 @@ PLATFORMS
60
61
  DEPENDENCIES
61
62
  byebug
62
63
  devup!
64
+ lefthook
63
65
  rake (~> 12.0)
64
66
  rspec (~> 3.0)
65
67
  simplecov
data/README.md CHANGED
@@ -1,96 +1,112 @@
1
- # DevUp!
1
+ # DevUp! [![Travis (.com) branch](https://img.shields.io/travis/com/sergio-fry/devup/master)](https://travis-ci.com/github/sergio-fry/devup) [![Gem](https://img.shields.io/gem/v/devup)](https://rubygems.org/gems/devup) [![Code Climate coverage](https://img.shields.io/codeclimate/coverage/sergio-fry/devup)](https://codeclimate.com/github/sergio-fry/devup) [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/sergio-fry/devup)](https://codeclimate.com/github/sergio-fry/devup) [![Gem](https://img.shields.io/gem/dt/devup)](https://rubygems.org/gems/devup)
2
2
 
3
- [![Travis (.com) branch](https://img.shields.io/travis/com/sergio-fry/devup/master)](https://travis-ci.com/github/sergio-fry/devup)
4
- [![Gem](https://img.shields.io/gem/v/devup)](https://rubygems.org/gems/devup)
5
- [![Code Climate coverage](https://img.shields.io/codeclimate/coverage/sergio-fry/devup)](https://codeclimate.com/github/sergio-fry/devup)
6
- [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/sergio-fry/devup)](https://codeclimate.com/github/sergio-fry/devup)
7
- [![Gem](https://img.shields.io/gem/dt/devup)](https://rubygems.org/gems/devup)
3
+ Describe development dependencies with docker-compose. It is not required to remember any fancy command to start docker. Just start developing your app. Rails is a first-class citizen, but could be used without ruby.
8
4
 
9
- **DevUp!** is a tool to run dev dependencies. It builds ENV vars with dynamic exposed ports for services defined in a docker-compose.yml to access from application.
10
5
 
11
- ![demo](demo.gif)
6
+ ## Requirements
12
7
 
13
- ## Installation
8
+ * Docker (>= 19.03.8)
9
+ * Docker Compose (>= 1.25.5)
14
10
 
15
- $ gem install devup
16
11
 
17
- ## Usage
18
12
 
13
+ ## Rails
19
14
 
20
- Create a docker-compose.yml with app dependencies like:
15
+ Create a docker-compose.devup.yml with app dependencies like:
21
16
 
22
17
  ```yaml
23
- version: '3'
18
+ version: "3"
24
19
 
25
20
  services:
26
21
  postgres:
27
- image: postgres
22
+ image: postgres:10-alpine
28
23
  ports:
29
24
  - "5432"
30
25
  ```
31
26
 
32
- Add devup/dotenv to your Gemfile:
33
-
34
- gem "devup"
27
+ Add **DevUp!** to your Gemfile
35
28
 
36
- For each service from docker-compose.yml **DevUp!** will export ENV variable like
29
+ ```ruby
30
+ gem "devup", group: [:development, :test]
31
+ ```
37
32
 
38
- POSTGRES_HOST=0.0.0.0
39
- POSTGRES_PORT=5432
33
+ and
40
34
 
41
- ### Rails
35
+ $ bundle install
42
36
 
43
37
 
44
38
  Update your database.yml to use ENV:
45
39
 
46
40
  ```yaml
47
- default: &default
48
- adapter: postgresql
49
- encoding: unicode
50
- host: <%= ENV.fetch("POSTGRES_HOST") %>
51
- port: <%= ENV.fetch("POSTGRES_PORT") %>
52
- username: postgres
53
- password:
41
+ test:
42
+ url: <%= ENV.fetch("DATABASE_URL") %>
54
43
 
55
44
  development:
56
- <<: *default
57
- database: development
45
+ url: <%= ENV.fetch("DATABASE_URL") %>
58
46
 
59
- test:
60
- <<: *default
61
- database: test
62
47
  ```
63
48
 
64
49
 
65
- You are ready to start rails
50
+ You are ready to use rails with PostgreSQL configured
51
+
52
+ $ RAILS_ENV=test bundle exec rake db:create
53
+ DevUp! INFO starting up...
54
+ DevUp! INFO up
66
55
 
67
- $ bundle exec rake db:create db:migrate
68
- $ bundle exec rails server
56
+ $ Created database 'dummy_rails_test'
69
57
 
70
58
 
71
- ### Without Rails
59
+ ## Without Rails
60
+
61
+ ENV vars from .env.services are loaded with dotenv automatically.
72
62
 
73
63
 
74
64
  ```ruby
75
65
  require "devup"
76
66
  require "sequel"
77
67
 
78
- DB = Sequel.connect("postgres://postgres@#{ENV.fetch( "POSTGRES_HOST" )}:#{ENV.fetch("POSTGRES_PORT")}/database_name")
68
+ DB = Sequel.connect(ENV.fetch("DATABASE_URL"))
79
69
  ```
80
70
 
81
71
 
82
- ### Without Ruby (PHP, nodejs, Java, ...)
72
+ ## Without Ruby (PHP, nodejs, Java, ...)
73
+
74
+ Install DevUp!
75
+
76
+ $ gem install devup
83
77
 
84
78
  Start up services
85
79
 
86
80
  $ devup
81
+ DevUp! INFO starting up...
82
+ DevUp! INFO up
83
+
84
+ $ cat .env.services
85
+ export POSTGRES_HOST=0.0.0.0
86
+ export POSTGRES_PORT=32944
87
+ export POSTGRES_PORT_5432=32944
88
+ export MEMCACHED_HOST=0.0.0.0
89
+ export MEMCACHED_PORT=32943
90
+ ...
87
91
 
88
- Load ENV vars from generated .env.services
92
+ Use your favourite dotenv extension to load vars from .env.services ([node-dotenv](https://www.npmjs.com/package/dotenv), [python-dotenv](https://pypi.org/project/python-dotenv/), [phpdotenv](https://github.com/vlucas/phpdotenv), ...)
93
+
94
+ Or load ENV vars manually
89
95
 
90
96
  $ source .env.services
91
97
 
92
98
  Now you can run app
93
99
 
100
+ ## How To
101
+
102
+ ### Disable **DevUp!**
103
+
104
+ If you don't want devup to setup your dev services, you can disable it by using `DEVUP_ENABLED=false`. Just add it to .env.local file.
105
+
106
+ ### Debug
107
+
108
+ $ export DEVUP_LOG_LEVEL=debug
109
+ $ devup
94
110
 
95
111
  ## Development
96
112
 
@@ -0,0 +1,8 @@
1
+ version: '3'
2
+
3
+ services:
4
+ postgres:
5
+ image: postgres:11
6
+ ports:
7
+ - "5432"
8
+
data/exe/devup CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  require "devup/environment"
4
4
 
5
- devup = Devup::Environment.new pwd: `pwd`
5
+ devup = Devup::Environment.new(
6
+ pwd: `pwd`,
7
+ logger: Devup::Logger.build(ENV.fetch("DEVUP_LOG_LEVEL", "info"))
8
+ )
6
9
 
7
10
  # TODO: use dry-cli here
8
11
  case ARGV[0]
@@ -0,0 +1,7 @@
1
+ pre-commit:
2
+ parallel: true
3
+ commands:
4
+ lint:
5
+ files: git diff --name-only --staged
6
+ glob: "*.rb"
7
+ run: standardrb --force-exclusion {files}
@@ -1,2 +1,15 @@
1
- devup = Devup::Environment.new pwd: `pwd`
2
- devup.up
1
+ if ENV.fetch("DEVUP_ENABLED", "true") == "true"
2
+ devup = Devup::Environment.new(
3
+ pwd: `pwd`,
4
+ logger: Devup::Logger.build(ENV.fetch("DEVUP_LOG_LEVEL", "info"))
5
+ )
6
+ devup.up
7
+
8
+ begin
9
+ require "spring/commands"
10
+
11
+ Spring.watch devup.root.join("docker-compose.yml")
12
+ rescue LoadError, ArgumentError
13
+ # Spring is not available
14
+ end
15
+ end
@@ -1,12 +1,22 @@
1
1
  require "yaml"
2
2
 
3
+ require "devup/compose/ps"
4
+
3
5
  module Devup
4
6
  class Compose
5
- attr_reader :path, :project
7
+ attr_reader :path, :project, :logger, :shell
8
+
9
+ class Error < StandardError; end
6
10
 
7
- def initialize(path, project: "devup")
11
+ def initialize(path, project: "devup", logger:, shell:)
8
12
  @path = path
9
13
  @project = project
14
+ @logger = logger
15
+ @shell = shell
16
+ end
17
+
18
+ def check
19
+ true
10
20
  end
11
21
 
12
22
  def services
@@ -14,11 +24,17 @@ module Devup
14
24
  end
15
25
 
16
26
  def service_ports(name)
17
- config["services"][name]["ports"]
27
+ config["services"][name]["ports"].map { |el| el.to_s.split(":")[-1].to_i }
28
+ end
29
+
30
+ def port_mapping(service, port)
31
+ exec("port #{service} #{port}").split(":")[-1].strip.to_i
18
32
  end
19
33
 
20
34
  def up
21
35
  exec "up -d --remove-orphans"
36
+
37
+ wait_alive 3
22
38
  end
23
39
 
24
40
  def stop
@@ -29,11 +45,33 @@ module Devup
29
45
  exec "rm -f"
30
46
  end
31
47
 
32
- def exec(cmd)
33
- `docker-compose -p #{project} -f #{path} #{cmd}`
48
+ private
49
+
50
+ def wait_alive(timeout, retry_sleep: 0.3)
51
+ start = Time.now
52
+
53
+ loop {
54
+ break if alive?
55
+
56
+ if Time.now - start > timeout
57
+ logger.error "can't run services"
58
+ break
59
+ end
60
+ sleep retry_sleep
61
+ }
34
62
  end
35
63
 
36
- private
64
+ def alive?
65
+ ComposeHelpers::Ps.new(exec("ps")).up?
66
+ end
67
+
68
+ def exec(cmd)
69
+ resp = shell.exec "docker-compose -p #{project} -f #{path} #{cmd}"
70
+
71
+ raise(Error) unless resp.success?
72
+
73
+ resp.data
74
+ end
37
75
 
38
76
  def config
39
77
  YAML.safe_load(config_content)
@@ -0,0 +1,30 @@
1
+ module Devup
2
+ module ComposeHelpers
3
+ class Ps
4
+ attr_reader :output
5
+ def initialize(output)
6
+ @output = output
7
+ end
8
+
9
+ def up?
10
+ service_lines.map { |line|
11
+ line.match(/Up/) && !line.match(/Exit/)
12
+ }.all?
13
+ end
14
+
15
+ def port_mapping(port)
16
+ m = output.match(/\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}:(\d+)->#{port}\/tcp/)
17
+
18
+ return if m.nil?
19
+
20
+ m[1].to_i
21
+ end
22
+
23
+ private
24
+
25
+ def service_lines
26
+ output.split("\n")[2..-1]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,52 +1,51 @@
1
1
  require "yaml"
2
2
 
3
+ require "devup/logger"
3
4
  require "devup/compose"
4
5
  require "devup/service"
6
+ require "devup/service_presenter"
7
+ require "devup/shell"
5
8
 
6
9
  module Devup
7
10
  class Environment
8
- attr_reader :pwd, :compose
11
+ attr_reader :pwd, :logger, :shell
9
12
 
10
- def initialize(pwd:, compose: nil)
13
+ def initialize(pwd:, compose: nil, logger: Logger.build, shell: Shell.new(pwd: pwd, logger: logger))
11
14
  @pwd = pwd.to_s.strip
12
- @compose = compose || Compose.new(root.join("docker-compose.yml"), project: project)
15
+ @compose = compose
16
+ @logger = logger
17
+ @shell = shell
13
18
  end
14
19
 
15
20
  def project
16
21
  pwd.split("/")[-1].strip
17
22
  end
18
23
 
19
- def vars
20
- compose.services.map { |name| Service.new(compose, name) }.map { |service|
21
- res = []
22
-
23
- res << {"#{service.name}_HOST".upcase => "0.0.0.0"}
24
-
25
- if service.ports.size > 0
26
- res << {"#{service.name}_PORT".upcase => service.ports.first.to}
27
-
28
- service.ports.each do |port|
29
- res << {"#{service.name}_PORT_#{port.from}".upcase => port.to}
30
- end
31
- end
32
-
33
- res
34
- }.flatten
35
- end
36
-
37
24
  def env
38
- vars.reduce({}, :merge).map { |k, v| "export #{k}=#{v}" }.join("\n")
25
+ services.map { |s| service_env(s) }.join("\n\n")
39
26
  end
40
27
 
41
28
  def up
29
+ logger.info "starting up..."
30
+ check
42
31
  compose.up
43
32
  write_dotenv
33
+ logger.info "up"
34
+ rescue => ex
35
+ clear_dotenv
36
+ logger.debug ex
37
+ logger.error "halted"
44
38
  end
45
39
 
46
40
  def down
41
+ logger.info "shutting down..."
47
42
  compose.stop
48
43
  compose.rm
49
44
  clear_dotenv
45
+ logger.info "down"
46
+ rescue => ex
47
+ logger.debug ex
48
+ logger.info "halted"
50
49
  end
51
50
 
52
51
  def root
@@ -55,6 +54,29 @@ module Devup
55
54
 
56
55
  private
57
56
 
57
+ def check
58
+ raise if missing_config
59
+ compose.check
60
+ end
61
+
62
+ def missing_config
63
+ if File.exist?(compose.path)
64
+ false
65
+ else
66
+ logger.error "missing #{compose.path}"
67
+
68
+ true
69
+ end
70
+ end
71
+
72
+ def services
73
+ @services ||= compose.services.map { |name| Service.new(compose, name) }
74
+ end
75
+
76
+ def service_env(service)
77
+ ServicePresenter.new(service, project: project).call
78
+ end
79
+
58
80
  def write_dotenv
59
81
  File.open(root.join(".env.services"), "w") { |f| f.write dotenv }
60
82
  end
@@ -70,10 +92,21 @@ module Devup
70
92
  # Home: https://github.com/sergio-fry/devup #
71
93
  ####################################################
72
94
  # START
95
+
73
96
  #{env}
97
+
74
98
  # END
75
99
 
76
100
  DOTENV
77
101
  end
102
+
103
+ def compose
104
+ @compose ||= begin
105
+ Compose.new(
106
+ root.join("docker-compose.devup.yml"),
107
+ project: project, logger: logger, shell: shell
108
+ )
109
+ end
110
+ end
78
111
  end
79
112
  end
@@ -0,0 +1,14 @@
1
+ require "logger"
2
+
3
+ module Devup
4
+ class Logger < ::Logger
5
+ def self.build(level = :info)
6
+ formatter = ->(severity, _time, _progname, msg) { "DevUp! #{severity} #{msg}\n" }
7
+ logger = new(STDOUT, formatter: formatter)
8
+
9
+ logger.level = level
10
+
11
+ logger
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ require "socket"
2
+ require "timeout"
3
+
4
+ module Devup
5
+ class PortChecker
6
+ def call(port)
7
+ s = TCPSocket.new("0.0.0.0", port)
8
+ s.write "1"
9
+ s.close
10
+
11
+ true
12
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
13
+ false
14
+ end
15
+ end
16
+ end
@@ -10,10 +10,16 @@ module Devup
10
10
  end
11
11
 
12
12
  def ports
13
- compose.service_ports(name).map { |el| el.to_s.split(":")[-1] }.map do |from|
13
+ @ports ||= fetch_ports
14
+ end
15
+
16
+ private
17
+
18
+ def fetch_ports
19
+ compose.service_ports(name).map do |from|
14
20
  OpenStruct.new(
15
21
  from: from,
16
- to: compose.exec("port #{name} #{from}").split(":")[-1].strip
22
+ to: compose.port_mapping(name, from)
17
23
  )
18
24
  end
19
25
  end
@@ -0,0 +1,63 @@
1
+ module Devup
2
+ class ServicePresenter
3
+ attr_reader :service, :project
4
+
5
+ def initialize(service, project: nil)
6
+ @service = service
7
+ @project = project
8
+ end
9
+
10
+ def call
11
+ res = []
12
+
13
+ res << "# #{service.name}"
14
+
15
+ if has_ports?
16
+ res << host_env
17
+ res << ports_env
18
+ else
19
+ res << "# no exposed ports"
20
+ end
21
+
22
+ res.join "\n"
23
+ end
24
+
25
+ private
26
+
27
+ def host_env
28
+ "export #{service.name.upcase}_HOST=0.0.0.0"
29
+ end
30
+
31
+ def ports_env
32
+ res = []
33
+
34
+ res << port_env(to: service.ports.first.to) if service.ports.size == 1
35
+
36
+ service.ports.each do |port|
37
+ res << port_env(from: port.from, to: port.to)
38
+ end
39
+
40
+ res.join "\n"
41
+ end
42
+
43
+ def port_env(from: nil, to:)
44
+ if from.nil?
45
+ "export #{service.name.upcase}_PORT=#{to}"
46
+ else
47
+ "export #{service.name.upcase}_PORT_#{from}=#{to}"
48
+ end
49
+ end
50
+
51
+ def has_ports?
52
+ service.ports.size > 0
53
+ end
54
+
55
+ def port_to(from)
56
+ service.ports.find { |p| p.from == from }&.to
57
+ end
58
+
59
+ def database_name
60
+ "db"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ require "open3"
2
+
3
+ module Devup
4
+ class Shell
5
+ attr_reader :pwd, :logger
6
+
7
+ def initialize(pwd:, logger:)
8
+ @pwd = pwd
9
+ @logger = logger
10
+ end
11
+
12
+ Result = Struct.new(:data, :status) {
13
+ def success?
14
+ status
15
+ end
16
+ }
17
+
18
+ def exec(cmd)
19
+ logger.debug "$ #{cmd}"
20
+
21
+ output, error, status = Open3.capture3(cmd + ";")
22
+
23
+ logger.error(error) unless status.success?
24
+
25
+ Result.new(output, status.success?)
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Devup
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergei O. Udalov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-05 00:00:00.000000000 Z
11
+ date: 2020-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dotenv
@@ -35,6 +35,7 @@ files:
35
35
  - ".gitignore"
36
36
  - ".rspec"
37
37
  - ".ruby-version"
38
+ - ".standard.yml"
38
39
  - ".travis.yml"
39
40
  - ".vimrc"
40
41
  - CODE_OF_CONDUCT.md
@@ -45,15 +46,21 @@ files:
45
46
  - Rakefile
46
47
  - bin/console
47
48
  - bin/setup
48
- - demo.gif
49
49
  - devup.gemspec
50
+ - docker-compose.devup.yml
50
51
  - exe/devup
52
+ - lefthook.yml
51
53
  - lib/devup.rb
52
54
  - lib/devup/boot.rb
53
55
  - lib/devup/compose.rb
56
+ - lib/devup/compose/ps.rb
54
57
  - lib/devup/dotenv.rb
55
58
  - lib/devup/environment.rb
59
+ - lib/devup/logger.rb
60
+ - lib/devup/port_checker.rb
56
61
  - lib/devup/service.rb
62
+ - lib/devup/service_presenter.rb
63
+ - lib/devup/shell.rb
57
64
  - lib/devup/version.rb
58
65
  homepage: https://github.com/sergio-fry/devup
59
66
  licenses:
data/demo.gif DELETED
Binary file