djin 0.4.0 → 0.8.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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +14 -0
- data/.rubocop.yml +8 -0
- data/.rubocop_todo.yml +17 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +6 -5
- data/README.md +122 -57
- data/Rakefile +6 -4
- data/Vertofile +4 -3
- data/bin/console +4 -3
- data/djin.gemspec +26 -23
- data/djin.yml +29 -20
- data/examples/djin.yml +25 -23
- data/exe/djin +2 -1
- data/lib/djin.rb +30 -23
- data/lib/djin/cli.rb +7 -5
- data/lib/djin/config_loader.rb +106 -0
- data/lib/djin/entities/file_config.rb +17 -0
- data/lib/djin/entities/task.rb +5 -5
- data/lib/djin/entities/types.rb +2 -0
- data/lib/djin/executor.rb +4 -11
- data/lib/djin/extensions/hash_extensions.rb +7 -1
- data/lib/djin/interpreter.rb +16 -63
- data/lib/djin/interpreter/base_command_builder.rb +26 -0
- data/lib/djin/interpreter/docker_command_builder.rb +29 -0
- data/lib/djin/interpreter/docker_compose_command_builder.rb +17 -0
- data/lib/djin/interpreter/local_command_builder.rb +11 -0
- data/lib/djin/repositories/task_repository.rb +2 -0
- data/lib/djin/task_contract.rb +38 -7
- data/lib/djin/version.rb +3 -1
- metadata +52 -30
- data/lib/djin/extensions/custom_predicates.rb +0 -18
data/djin.yml
CHANGED
@@ -1,26 +1,35 @@
|
|
1
|
-
djin_version: '0.
|
1
|
+
djin_version: '0.8.0'
|
2
2
|
|
3
3
|
_default_run_options: &default_run_options
|
4
4
|
options: "--rm --entrypoint=''"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
6
|
+
tasks:
|
7
|
+
test:
|
8
|
+
description: Runs Specs
|
9
|
+
docker-compose:
|
10
|
+
service: app
|
11
|
+
run:
|
12
|
+
commands: "cd /usr/src/djin && rspec {{args}}"
|
13
|
+
<<: *default_run_options
|
14
|
+
aliases:
|
15
|
+
- rspec
|
25
16
|
|
17
|
+
sh:
|
18
|
+
description: Enter app service shell
|
19
|
+
docker-compose:
|
20
|
+
service: app
|
21
|
+
run:
|
22
|
+
commands: "sh"
|
23
|
+
<<: *default_run_options
|
24
|
+
run:
|
25
|
+
docker-compose:
|
26
|
+
service: app
|
27
|
+
run:
|
28
|
+
commands: "sh -c '{{args}}'"
|
29
|
+
<<: *default_run_options
|
26
30
|
|
31
|
+
release:
|
32
|
+
local:
|
33
|
+
run:
|
34
|
+
- verto tag up {{args}}
|
35
|
+
- bundle exec rake release
|
data/examples/djin.yml
CHANGED
@@ -1,27 +1,29 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
docker:
|
4
|
-
image: "ruby:2.5"
|
5
|
-
run:
|
6
|
-
- "ruby -e 'puts \\\" Hello\\\"'"
|
7
|
-
test:
|
8
|
-
docker-compose:
|
9
|
-
service: app
|
10
|
-
run:
|
11
|
-
commands: rspec
|
12
|
-
options: "--rm"
|
2
|
+
djin_version: '0.8.0'
|
13
3
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
- "ruby
|
20
|
-
|
4
|
+
tasks:
|
5
|
+
default:
|
6
|
+
docker:
|
7
|
+
image: "ruby:2.5"
|
8
|
+
run:
|
9
|
+
- "ruby -e 'puts \\\" Hello\\\"'"
|
10
|
+
test:
|
11
|
+
docker-compose:
|
12
|
+
service: app
|
13
|
+
run:
|
14
|
+
commands: rspec
|
15
|
+
options: "--rm"
|
21
16
|
|
17
|
+
script:
|
18
|
+
docker:
|
19
|
+
image: "ruby:2.6"
|
20
|
+
run:
|
21
|
+
commands:
|
22
|
+
- "ruby /scripts/my_ruby_script.rb"
|
23
|
+
options: "--rm -v $(pwd)/my_ruby_script.rb:/scripts/my_ruby_script.rb"
|
22
24
|
|
23
|
-
with_build:
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
with_build:
|
26
|
+
docker:
|
27
|
+
build: .
|
28
|
+
run:
|
29
|
+
- "ruby -e 'puts \" Hello\"'"
|
data/exe/djin
CHANGED
data/lib/djin.rb
CHANGED
@@ -1,20 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
16
|
-
require
|
17
|
-
require
|
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/entities/file_config'
|
15
|
+
require 'djin/interpreter/base_command_builder'
|
16
|
+
require 'djin/interpreter/docker_command_builder'
|
17
|
+
require 'djin/interpreter/docker_compose_command_builder'
|
18
|
+
require 'djin/interpreter/local_command_builder'
|
19
|
+
require 'djin/interpreter'
|
20
|
+
require 'djin/config_loader'
|
21
|
+
require 'djin/executor'
|
22
|
+
require 'djin/cli'
|
23
|
+
require 'djin/task_contract'
|
24
|
+
require 'djin/repositories/task_repository'
|
18
25
|
|
19
26
|
module Djin
|
20
27
|
class Error < StandardError; end
|
@@ -22,15 +29,16 @@ module Djin
|
|
22
29
|
def self.load_tasks!(path = Pathname.getwd.join('djin.yml'))
|
23
30
|
abort 'Error: djin.yml not found' unless path.exist?
|
24
31
|
|
25
|
-
|
32
|
+
file_config = ConfigLoader.load!(path.read)
|
26
33
|
|
27
|
-
tasks
|
34
|
+
# TODO: Make all tasks be under 'tasks' key, passing only the tasks here
|
35
|
+
tasks = Interpreter.load!(file_config)
|
28
36
|
|
29
37
|
@task_repository = TaskRepository.new(tasks)
|
30
38
|
CLI.load_tasks!(tasks)
|
31
|
-
|
32
|
-
|
33
|
-
abort(
|
39
|
+
rescue Djin::Interpreter::InvalidConfigurationError => e
|
40
|
+
error_name = e.class.name.split('::').last
|
41
|
+
abort("[#{error_name}] #{e.message}")
|
34
42
|
end
|
35
43
|
|
36
44
|
def self.tasks
|
@@ -41,4 +49,3 @@ module Djin
|
|
41
49
|
@task_repository ||= TaskRepository.new
|
42
50
|
end
|
43
51
|
end
|
44
|
-
|
data/lib/djin/cli.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Djin
|
2
4
|
class CLI
|
3
5
|
extend Dry::CLI::Registry
|
@@ -5,21 +7,21 @@ module Djin
|
|
5
7
|
def self.load_tasks!(tasks)
|
6
8
|
tasks.each do |task|
|
7
9
|
command = Class.new(Dry::CLI::Command) do
|
8
|
-
desc
|
10
|
+
desc task.description
|
9
11
|
|
10
12
|
define_method(:task) { task }
|
11
13
|
|
12
|
-
def call(
|
13
|
-
Executor.new
|
14
|
+
def call(**)
|
15
|
+
Executor.new.call(task)
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
17
|
-
register
|
19
|
+
register(task.name, command, aliases: task.aliases)
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
23
|
class Version < Dry::CLI::Command
|
22
|
-
desc
|
24
|
+
desc 'Prints Djin Version'
|
23
25
|
|
24
26
|
def call(*)
|
25
27
|
puts Djin::VERSION
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Djin
|
4
|
+
# TODO: Refactor this class to be the Interpreter
|
5
|
+
# class and use the current interpreter as
|
6
|
+
# a TaskLoader
|
7
|
+
class ConfigLoader
|
8
|
+
using Djin::HashExtensions
|
9
|
+
RESERVED_WORDS = %w[djin_version variables tasks].freeze
|
10
|
+
|
11
|
+
def self.load!(template_file)
|
12
|
+
new(template_file).load!
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(template_file)
|
16
|
+
@template_file = template_file
|
17
|
+
end
|
18
|
+
|
19
|
+
def load!
|
20
|
+
validate_version!
|
21
|
+
|
22
|
+
file_config
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def file_config
|
28
|
+
FileConfig.new(
|
29
|
+
djin_version: version,
|
30
|
+
variables: variables,
|
31
|
+
tasks: tasks,
|
32
|
+
raw_tasks: raw_tasks
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def raw_djin_config
|
37
|
+
@raw_djin_config ||= yaml_load(@template_file)
|
38
|
+
rescue Psych::SyntaxError => e
|
39
|
+
raise Interpreter::InvalidConfigFileError, e.message
|
40
|
+
end
|
41
|
+
|
42
|
+
def rendered_djin_config
|
43
|
+
@rendered_djin_config ||= begin
|
44
|
+
locals = env.merge(variables)
|
45
|
+
|
46
|
+
rendered_yaml = Mustache.render(@template_file,
|
47
|
+
args: args.join(' '),
|
48
|
+
args?: args.any?,
|
49
|
+
**locals)
|
50
|
+
yaml_load(rendered_yaml)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def version
|
55
|
+
# TODO: Deprecates djin_version and use version instead
|
56
|
+
@version || raw_djin_config['djin_version']
|
57
|
+
end
|
58
|
+
|
59
|
+
def variables
|
60
|
+
@variables ||= raw_djin_config['variables']&.symbolize_keys || {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def tasks
|
64
|
+
rendered_djin_config['tasks'] || legacy_tasks
|
65
|
+
end
|
66
|
+
|
67
|
+
def raw_tasks
|
68
|
+
raw_djin_config['tasks'] || legacy_raw_tasks
|
69
|
+
end
|
70
|
+
|
71
|
+
def legacy_tasks
|
72
|
+
warn '[DEPRECATED] Root tasks are deprecated and will be removed in Djin 1.0.0,' \
|
73
|
+
' put the tasks under \'tasks\' keyword'
|
74
|
+
|
75
|
+
rendered_djin_config.except(*RESERVED_WORDS).reject { |task| task.start_with?('_') }
|
76
|
+
end
|
77
|
+
|
78
|
+
def legacy_raw_tasks
|
79
|
+
raw_djin_config.except(*RESERVED_WORDS).reject { |task| task.start_with?('_') }
|
80
|
+
end
|
81
|
+
|
82
|
+
def args
|
83
|
+
index = ARGV.index('--')
|
84
|
+
|
85
|
+
return [] unless index
|
86
|
+
|
87
|
+
ARGV.slice((index + 1)..ARGV.size)
|
88
|
+
end
|
89
|
+
|
90
|
+
def env
|
91
|
+
@env ||= ENV.to_h.symbolize_keys
|
92
|
+
end
|
93
|
+
|
94
|
+
def yaml_load(text)
|
95
|
+
YAML.safe_load(text, [], [], true)
|
96
|
+
end
|
97
|
+
|
98
|
+
def validate_version!
|
99
|
+
raise Interpreter::MissingVersionError, 'Missing djin_version' unless version
|
100
|
+
|
101
|
+
return if file_config.version_supported?
|
102
|
+
|
103
|
+
raise Interpreter::VersionNotSupportedError, "Version #{version} is not supported, use #{Djin::VERSION} or higher"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Djin
|
4
|
+
class FileConfig < Dry::Struct
|
5
|
+
attribute :djin_version, Types::String
|
6
|
+
attribute :variables, Types::Hash.optional.default({}.freeze)
|
7
|
+
attribute :tasks, Types::Hash
|
8
|
+
attribute :raw_tasks, Types::Hash
|
9
|
+
# TODO: Add env and args
|
10
|
+
|
11
|
+
include Dry::Equalizer(:djin_version, :variables, :tasks, :raw_tasks)
|
12
|
+
|
13
|
+
def version_supported?
|
14
|
+
Vseries::SemanticVersion.new(Djin::VERSION) >= Vseries::SemanticVersion.new(djin_version)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/djin/entities/task.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
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
8
|
attribute :command, Types::String.optional.default(nil)
|
9
|
+
attribute :raw_command, Types::String.optional.default(nil)
|
10
|
+
attribute :aliases, Types::Array.of(Types::String).optional.default([].freeze)
|
7
11
|
attribute :depends_on, Types::Array.of(Types::String).optional.default([].freeze)
|
8
12
|
|
9
|
-
|
10
|
-
name == other.name &&
|
11
|
-
command == other.command &&
|
12
|
-
build_command == other.build_command
|
13
|
-
end
|
13
|
+
include Dry::Equalizer(:name, :command, :build_command)
|
14
14
|
end
|
15
15
|
end
|
data/lib/djin/entities/types.rb
CHANGED
data/lib/djin/executor.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Djin
|
2
4
|
class Executor
|
3
|
-
def initialize(task_repository: Djin.task_repository
|
5
|
+
def initialize(task_repository: Djin.task_repository)
|
4
6
|
@task_repository = task_repository
|
5
|
-
@args = args
|
6
7
|
end
|
7
8
|
|
8
9
|
def call(*tasks)
|
@@ -23,15 +24,7 @@ module Djin
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def run(command)
|
26
|
-
|
27
|
-
args: @args.join(' '),
|
28
|
-
args?: @args.any?,
|
29
|
-
**env)
|
30
|
-
system command_with_args
|
31
|
-
end
|
32
|
-
|
33
|
-
def env
|
34
|
-
@env = ENV.to_h.map {|k,v| [k.to_sym, v]}.to_h
|
27
|
+
system command
|
35
28
|
end
|
36
29
|
end
|
37
30
|
end
|
@@ -1,8 +1,14 @@
|
|
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) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def symbolize_keys
|
11
|
+
map { |key, value| [key.to_sym, value] }.to_h
|
6
12
|
end
|
7
13
|
end
|
8
14
|
end
|
data/lib/djin/interpreter.rb
CHANGED
@@ -1,34 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Djin
|
2
4
|
class Interpreter
|
3
5
|
using Djin::HashExtensions
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
+
# TODO: Move Errors to ConfigLoader
|
7
8
|
InvalidConfigurationError = Class.new(StandardError)
|
9
|
+
InvalidConfigFileError = Class.new(InvalidConfigurationError)
|
8
10
|
MissingVersionError = Class.new(InvalidConfigurationError)
|
9
11
|
VersionNotSupportedError = Class.new(InvalidConfigurationError)
|
10
12
|
InvalidSyntaxError = Class.new(InvalidConfigurationError)
|
11
13
|
|
12
14
|
class << self
|
13
|
-
def load!(
|
14
|
-
version = params['djin_version']
|
15
|
-
raise MissingVersionError, 'Missing djin_version' unless version
|
16
|
-
raise VersionNotSupportedError, "Version #{version} is not supported, use #{Djin::VERSION} or higher" unless version_supported?(version)
|
17
|
-
|
18
|
-
tasks_params = params.except(*RESERVED_WORDS).reject { |task| task.start_with?('_') }
|
15
|
+
def load!(file_config)
|
19
16
|
contract = TaskContract.new
|
20
17
|
|
21
|
-
|
18
|
+
file_config.tasks.map do |task_name, options|
|
22
19
|
result = contract.call(options)
|
23
20
|
|
24
21
|
raise InvalidSyntaxError, { task_name.to_sym => result.errors.to_h } if result.failure?
|
25
22
|
|
26
23
|
command, build_command = build_commands(options, task_name: task_name)
|
27
24
|
|
25
|
+
raw_command, = build_commands(file_config.raw_tasks[task_name], task_name: task_name)
|
26
|
+
|
28
27
|
task_params = {
|
29
28
|
name: task_name,
|
30
29
|
build_command: build_command,
|
30
|
+
description: options['description'] || "Runs: #{raw_command}",
|
31
31
|
command: command,
|
32
|
+
raw_command: raw_command,
|
33
|
+
aliases: options['aliases'],
|
32
34
|
depends_on: options['depends_on']
|
33
35
|
}.compact
|
34
36
|
|
@@ -42,62 +44,13 @@ module Djin
|
|
42
44
|
# Validate that only one ot the two is passed
|
43
45
|
docker_params = params['docker']
|
44
46
|
docker_compose_params = params['docker-compose']
|
47
|
+
local_params = params['local']
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
def build_docker_commands(params, task_name:)
|
51
|
-
current_folder_name = Pathname.getwd.basename.to_s
|
52
|
-
image = params['image'] || "djin_#{current_folder_name}_#{task_name}"
|
53
|
-
|
54
|
-
build_params = params['build']
|
55
|
-
|
56
|
-
if build_params.is_a?(Hash)
|
57
|
-
build_context = build_params['context']
|
58
|
-
build_options = build_params['options']
|
59
|
-
end
|
60
|
-
|
61
|
-
build_context ||= build_params
|
62
|
-
|
63
|
-
run_command, run_options = build_run_params(params['run'])
|
64
|
-
|
65
|
-
command = %Q{docker run #{run_options} #{image} sh -c "#{run_command}"}.squeeze(' ')
|
66
|
-
|
67
|
-
build_command = "docker build #{build_context} #{build_options} -t #{image}".squeeze(' ') if build_context
|
68
|
-
|
69
|
-
[command, build_command]
|
70
|
-
end
|
71
|
-
|
72
|
-
def build_docker_compose_commands(params)
|
73
|
-
service = params['service']
|
74
|
-
|
75
|
-
compose_options = params['options']
|
76
|
-
|
77
|
-
run_command, run_options = build_run_params(params['run'])
|
78
|
-
|
79
|
-
[%Q{docker-compose #{compose_options} run #{run_options} #{service} sh -c "#{run_command}"}.squeeze(' '), nil]
|
80
|
-
end
|
81
|
-
|
82
|
-
def build_run_params(run_params)
|
83
|
-
run_command = run_params
|
84
|
-
|
85
|
-
if run_params.is_a?(Hash)
|
86
|
-
run_command = run_params['commands']
|
87
|
-
run_options = run_params['options']
|
88
|
-
end
|
89
|
-
|
90
|
-
run_command = run_command.join(' && ') if run_command.is_a?(Array)
|
91
|
-
|
92
|
-
[run_command, run_options]
|
93
|
-
end
|
94
|
-
|
95
|
-
def validate_version!(version)
|
96
|
-
|
97
|
-
end
|
49
|
+
# TODO: Refactor to use chain of responsability
|
50
|
+
return DockerCommandBuilder.call(docker_params, task_name: task_name) if docker_params
|
51
|
+
return DockerComposeCommandBuilder.call(docker_compose_params) if docker_compose_params
|
98
52
|
|
99
|
-
|
100
|
-
Vseries::SemanticVersion.new(Djin::VERSION) >= Vseries::SemanticVersion.new(version)
|
53
|
+
LocalCommandBuilder.call(local_params) if local_params
|
101
54
|
end
|
102
55
|
end
|
103
56
|
end
|