djin 0.1.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,35 +1,40 @@
1
- lib = File.expand_path("lib", __dir__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require "djin/version"
5
+ require 'djin/version'
4
6
 
5
7
  Gem::Specification.new do |spec|
6
- spec.name = "djin"
8
+ spec.name = 'djin'
7
9
  spec.version = Djin::VERSION
8
- spec.authors = ["Carlos Atkinson"]
9
- spec.email = ["carlos.atks@gmail.com"]
10
+ spec.authors = ['Carlos Atkinson']
11
+ spec.email = ['carlos.atks@gmail.com']
10
12
 
11
- spec.summary = %q{djin is a make-like utility for docker containers}
12
- spec.homepage = "https://github.com/catks/djin"
13
- spec.license = "MIT"
13
+ spec.summary = 'djin is a make-like utility for docker containers'
14
+ spec.homepage = 'https://github.com/catks/djin'
15
+ spec.license = 'MIT'
14
16
 
15
17
  # spec.metadata["homepage_uri"] = spec.homepage
16
- #spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
17
- #spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
18
+ # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
19
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
18
20
 
19
21
  # Specify which files should be added to the gem when it is released.
20
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
24
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
25
  end
24
- spec.bindir = "exe"
26
+ spec.bindir = 'exe'
25
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
- spec.require_paths = ["lib"]
28
+ spec.require_paths = ['lib']
27
29
 
28
- spec.add_dependency "dry-struct", "~> 1.3.0"
29
- spec.add_dependency "dry-cli", "~> 0.5.0"
30
- spec.add_dependency "dry-validation", "~> 1.4.0"
31
- spec.add_development_dependency "bundler", "~> 2.0"
32
- spec.add_development_dependency "rake", "~> 10.0"
33
- spec.add_development_dependency "rspec", "~> 3.0"
34
- spec.add_development_dependency "byebug"
30
+ spec.add_dependency 'dry-cli', '~> 0.6.0'
31
+ spec.add_dependency 'dry-struct', '~> 1.3.0'
32
+ spec.add_dependency 'dry-validation', '~> 1.5.1'
33
+ spec.add_dependency 'mustache', '~> 1.1.1'
34
+ spec.add_dependency 'vseries', '~> 0.1.0'
35
+ spec.add_development_dependency 'bundler', '~> 2.0'
36
+ spec.add_development_dependency 'byebug'
37
+ spec.add_development_dependency 'rake', '~> 13.0'
38
+ spec.add_development_dependency 'rspec', '~> 3.0'
39
+ spec.add_development_dependency 'rubocop'
35
40
  end
data/djin.yml CHANGED
@@ -1,7 +1,33 @@
1
+ djin_version: '0.5.0'
2
+
3
+ _default_run_options: &default_run_options
4
+ options: "--rm --entrypoint=''"
5
+
1
6
  test:
2
7
  docker-compose:
3
8
  service: app
4
9
  run:
5
- commands: "cd /usr/src/djin && rspec"
6
- options: "--rm --entrypoint=''"
10
+ commands: "cd /usr/src/djin && rspec {{args}}"
11
+ <<: *default_run_options
12
+
13
+ sh:
14
+ docker-compose:
15
+ service: app
16
+ run:
17
+ commands: "sh"
18
+ <<: *default_run_options
19
+ run:
20
+ docker-compose:
21
+ service: app
22
+ run:
23
+ commands: "sh -c '{{args}}'"
24
+ <<: *default_run_options
25
+
26
+ release:
27
+ local:
28
+ run:
29
+ - verto tag up {{args}}
30
+ - rake release
31
+
32
+
7
33
 
@@ -1,4 +1,6 @@
1
1
  ---
2
+ djin_version: '0.5.0'
3
+
2
4
  default:
3
5
  docker:
4
6
  image: "ruby:2.5"
@@ -8,7 +10,7 @@ test:
8
10
  docker-compose:
9
11
  service: app
10
12
  run:
11
- command: rspec
13
+ commands: rspec
12
14
  options: "--rm"
13
15
 
14
16
  script:
data/exe/djin CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ Signal.trap('INT') { exit 2 }
2
5
 
3
6
  require_relative '../lib/djin'
4
7
 
@@ -1,36 +1,49 @@
1
- require "djin/version"
2
- require "pathname"
3
- require "yaml"
4
- require "dry-struct"
5
- require "dry-validation"
6
- require "dry/cli"
7
- require "djin/extensions/hash_extensions"
8
- require "djin/extensions/custom_predicates"
9
- require "djin/entities/types"
10
- require "djin/entities/task"
11
- require "djin/interpreter"
12
- require "djin/executor"
13
- require "djin/cli"
14
- require "djin/task_contract"
1
+ # frozen_string_literal: true
2
+
3
+ require 'djin/version'
4
+ require 'pathname'
5
+ require 'yaml'
6
+ require 'dry-struct'
7
+ require 'dry-validation'
8
+ require 'vseries'
9
+ require 'dry/cli'
10
+ require 'mustache'
11
+ require 'djin/extensions/hash_extensions'
12
+ require 'djin/entities/types'
13
+ require 'djin/entities/task'
14
+ require 'djin/interpreter/base_command_builder'
15
+ require 'djin/interpreter/docker_command_builder'
16
+ require 'djin/interpreter/docker_compose_command_builder'
17
+ require 'djin/interpreter/local_command_builder'
18
+ require 'djin/interpreter'
19
+ require 'djin/template_renderer'
20
+ require 'djin/executor'
21
+ require 'djin/cli'
22
+ require 'djin/task_contract'
23
+ require 'djin/repositories/task_repository'
15
24
 
16
25
  module Djin
17
26
  class Error < StandardError; end
18
- # Your code goes here...
19
27
 
20
28
  def self.load_tasks!(path = Pathname.getwd.join('djin.yml'))
21
29
  abort 'Error: djin.yml not found' unless path.exist?
22
30
 
23
- djin_file = YAML.load(path.read)
24
- @tasks = Djin::Interpreter.load!(djin_file)
31
+ rendered_djin_file = TemplateRenderer.render(path.read)
32
+ djin_config = YAML.safe_load(rendered_djin_file, [], [], true)
25
33
 
26
- CLI.load_tasks!(@tasks)
34
+ tasks = Interpreter.load!(djin_config)
27
35
 
28
- rescue Djin::Interpreter::InvalidSyntax => ex
29
- abort(ex.message)
36
+ @task_repository = TaskRepository.new(tasks)
37
+ CLI.load_tasks!(tasks)
38
+ rescue Djin::Interpreter::InvalidConfigurationError => e
39
+ abort(e.message)
30
40
  end
31
41
 
32
42
  def self.tasks
33
- @tasks || []
43
+ task_repository.all
34
44
  end
35
- end
36
45
 
46
+ def self.task_repository
47
+ @task_repository ||= TaskRepository.new
48
+ end
49
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Djin
2
4
  class CLI
3
5
  extend Dry::CLI::Registry
@@ -9,7 +11,7 @@ module Djin
9
11
 
10
12
  define_method(:task) { task }
11
13
 
12
- def call(**options)
14
+ def call(**)
13
15
  Executor.new.call(task)
14
16
  end
15
17
  end
@@ -17,5 +19,15 @@ module Djin
17
19
  register task.name, command
18
20
  end
19
21
  end
22
+
23
+ class Version < Dry::CLI::Command
24
+ desc 'Prints Djin Version'
25
+
26
+ def call(*)
27
+ puts Djin::VERSION
28
+ end
29
+ end
30
+
31
+ register '--version', Version, aliases: ['-v']
20
32
  end
21
33
  end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Djin
2
4
  class Task < Dry::Struct
3
5
  attribute :name, Types::String
4
6
  attribute :description, Types::String.optional.default(nil)
5
7
  attribute :build_command, Types::String.optional.default(nil)
6
- attribute :command, Types::String
8
+ attribute :command, Types::String.optional.default(nil)
9
+ attribute :depends_on, Types::Array.of(Types::String).optional.default([].freeze)
7
10
 
8
11
  def ==(other)
9
12
  name == other.name &&
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Djin
2
4
  module Types
3
5
  include Dry.Types()
@@ -1,16 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Djin
2
4
  class Executor
3
- def call(*tasks)
4
- tasks.each do |task|
5
- run task.build_command if task.build_command
6
- run task.command
7
- end
8
- end
5
+ def initialize(task_repository: Djin.task_repository)
6
+ @task_repository = task_repository
7
+ end
8
+
9
+ def call(*tasks)
10
+ tasks.each do |task|
11
+ run_task(task)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def run_task(task)
18
+ @task_repository.find_by_names(task.depends_on).each do |dependent_task|
19
+ run_task dependent_task
20
+ end
9
21
 
10
- private
22
+ run task.build_command if task.build_command
23
+ run task.command if task.command
24
+ end
11
25
 
12
- def run(command)
13
- system command
14
- end
26
+ def run(command)
27
+ system command
28
+ end
15
29
  end
16
30
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Djin
2
4
  module HashExtensions
3
5
  refine Hash do
4
6
  def except(*keys)
5
- reject { |key,_| keys.include?(key) }
7
+ reject { |key, _| keys.include?(key) }
6
8
  end
7
9
  end
8
10
  end
@@ -1,23 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Djin
2
4
  class Interpreter
3
5
  using Djin::HashExtensions
4
6
 
5
- RESERVED_WORDS = %w[_version _default_options].freeze
7
+ RESERVED_WORDS = %w[djin_version _default_options].freeze
6
8
 
7
- InvalidSyntax = Class.new(StandardError)
9
+ InvalidConfigurationError = Class.new(StandardError)
10
+ MissingVersionError = Class.new(InvalidConfigurationError)
11
+ VersionNotSupportedError = Class.new(InvalidConfigurationError)
12
+ InvalidSyntaxError = Class.new(InvalidConfigurationError)
8
13
 
9
14
  class << self
10
15
  def load!(params)
11
- task_params = params.except(*RESERVED_WORDS)
16
+ version = params['djin_version']
17
+ raise MissingVersionError, 'Missing djin_version' unless version
18
+ unless version_supported?(version)
19
+ raise VersionNotSupportedError, "Version #{version} is not supported, use #{Djin::VERSION} or higher"
20
+ end
21
+
22
+ tasks_params = params.except(*RESERVED_WORDS).reject { |task| task.start_with?('_') }
12
23
  contract = TaskContract.new
13
24
 
14
- task_params.map do |task_name, options|
25
+ tasks_params.map do |task_name, options|
15
26
  result = contract.call(options)
16
27
 
17
- raise InvalidSyntax, result.errors.to_h if result.failure?
28
+ raise InvalidSyntaxError, { task_name.to_sym => result.errors.to_h } if result.failure?
18
29
 
19
30
  command, build_command = build_commands(options, task_name: task_name)
20
- Djin::Task.new(name: task_name, build_command: build_command, command: command)
31
+
32
+ task_params = {
33
+ name: task_name,
34
+ build_command: build_command,
35
+ command: command,
36
+ depends_on: options['depends_on']
37
+ }.compact
38
+
39
+ Djin::Task.new(**task_params)
21
40
  end
22
41
  end
23
42
 
@@ -27,54 +46,17 @@ module Djin
27
46
  # Validate that only one ot the two is passed
28
47
  docker_params = params['docker']
29
48
  docker_compose_params = params['docker-compose']
49
+ local_params = params['local']
30
50
 
31
- return build_docker_commands(docker_params, task_name: task_name) if docker_params
32
- build_docker_compose_commands(docker_compose_params) if docker_compose_params
33
- end
34
-
35
- def build_docker_commands(params, task_name:)
36
- current_folder_name = Pathname.getwd.basename.to_s
37
- image = params['image'] || "djin_#{current_folder_name}_#{task_name}"
38
-
39
- build_params = params['build']
40
-
41
- if build_params.is_a?(Hash)
42
- build_context = build_params['context']
43
- build_options = build_params['options']
44
- end
45
-
46
- build_context ||= build_params
47
-
48
- run_command, run_options = build_run_params(params['run'])
51
+ # TODO: Refactor to use chain of responsability
52
+ return DockerCommandBuilder.call(docker_params, task_name: task_name) if docker_params
53
+ return DockerComposeCommandBuilder.call(docker_compose_params) if docker_compose_params
49
54
 
50
- command = %Q{docker run #{run_options} #{image} sh -c "#{run_command}"}.squeeze(' ')
51
-
52
- build_command = "docker build #{build_context} #{build_options} -t #{image}".squeeze(' ') if build_context
53
-
54
- [command, build_command]
55
+ LocalCommandBuilder.call(local_params) if local_params
55
56
  end
56
57
 
57
- def build_docker_compose_commands(params)
58
- service = params['service']
59
-
60
- compose_options = params['options']
61
-
62
- run_command, run_options = build_run_params(params['run'])
63
-
64
- [%Q{docker-compose #{compose_options} run #{run_options} #{service} sh -c "#{run_command}"}.squeeze(' '), nil]
65
- end
66
-
67
- def build_run_params(run_params)
68
- run_command = run_params
69
-
70
- if run_params.is_a?(Hash)
71
- run_command = run_params['commands']
72
- run_options = run_params['options']
73
- end
74
-
75
- run_command = run_command.join(' && ') if run_command.is_a?(Array)
76
-
77
- [run_command, run_options]
58
+ def version_supported?(version)
59
+ Vseries::SemanticVersion.new(Djin::VERSION) >= Vseries::SemanticVersion.new(version)
78
60
  end
79
61
  end
80
62
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Djin
4
+ class Interpreter
5
+ class BaseCommandBuilder
6
+ def self.call(*options)
7
+ new.call(*options)
8
+ end
9
+
10
+ private
11
+
12
+ def build_run_params(run_params)
13
+ run_command = run_params
14
+
15
+ if run_params.is_a?(Hash)
16
+ run_command = run_params['commands']
17
+ run_options = run_params['options']
18
+ end
19
+
20
+ run_command = run_command.join(' && ') if run_command.is_a?(Array)
21
+
22
+ [run_command, run_options]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Djin
4
+ class Interpreter
5
+ class DockerCommandBuilder < BaseCommandBuilder
6
+ def call(params, task_name:)
7
+ current_folder_name = Pathname.getwd.basename.to_s
8
+ image = params['image'] || "djin_#{current_folder_name}_#{task_name}"
9
+
10
+ build_params = params['build']
11
+
12
+ if build_params.is_a?(Hash)
13
+ build_context = build_params['context']
14
+ build_options = build_params['options']
15
+ end
16
+
17
+ build_context ||= build_params
18
+
19
+ run_command, run_options = build_run_params(params['run'])
20
+
21
+ command = %(docker run #{run_options} #{image} sh -c "#{run_command}").squeeze(' ')
22
+
23
+ build_command = "docker build #{build_context} #{build_options} -t #{image}".squeeze(' ') if build_context
24
+
25
+ [command, build_command]
26
+ end
27
+ end
28
+ end
29
+ end