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.
data/djin.yml CHANGED
@@ -1,26 +1,35 @@
1
- djin_version: '0.2.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
- test:
7
- docker-compose:
8
- service: app
9
- run:
10
- commands: "cd /usr/src/djin && rspec"
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
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
@@ -1,27 +1,29 @@
1
1
  ---
2
- default:
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
- script:
15
- docker:
16
- image: "ruby:2.6"
17
- run:
18
- commands:
19
- - "ruby /scripts/my_ruby_script.rb"
20
- options: "--rm -v $(pwd)/my_ruby_script.rb:/scripts/my_ruby_script.rb"
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
- docker:
25
- build: .
26
- run:
27
- - "ruby -e 'puts \" Hello\"'"
25
+ with_build:
26
+ docker:
27
+ build: .
28
+ run:
29
+ - "ruby -e 'puts \" Hello\"'"
data/exe/djin CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- Signal.trap("INT") { exit 2 }
4
+ Signal.trap('INT') { exit 2 }
4
5
 
5
6
  require_relative '../lib/djin'
6
7
 
@@ -1,20 +1,27 @@
1
- require "djin/version"
2
- require "pathname"
3
- require "yaml"
4
- require "dry-struct"
5
- require "dry-validation"
6
- require "vseries"
7
- require "dry/cli"
8
- require "mustache"
9
- require "djin/extensions/hash_extensions"
10
- require "djin/extensions/custom_predicates"
11
- require "djin/entities/types"
12
- require "djin/entities/task"
13
- require "djin/interpreter"
14
- require "djin/executor"
15
- require "djin/cli"
16
- require "djin/task_contract"
17
- require "djin/repositories/task_repository"
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
- djin_file = YAML.safe_load(path.read, [], [], true)
32
+ file_config = ConfigLoader.load!(path.read)
26
33
 
27
- tasks = Djin::Interpreter.load!(djin_file)
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
- rescue Djin::Interpreter::InvalidConfigurationError => ex
33
- abort(ex.message)
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
-
@@ -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 "Runs: #{task.command}"
10
+ desc task.description
9
11
 
10
12
  define_method(:task) { task }
11
13
 
12
- def call(args: [], **)
13
- Executor.new(args: args).call(task)
14
+ def call(**)
15
+ Executor.new.call(task)
14
16
  end
15
17
  end
16
18
 
17
- register task.name, command
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 "Prints Djin Version"
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
@@ -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
- def ==(other)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Djin
2
4
  module Types
3
5
  include Dry.Types()
@@ -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, args: [])
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
- command_with_args = Mustache.render(command,
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
@@ -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
- RESERVED_WORDS = %w[djin_version _default_options].freeze
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!(params)
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
- tasks_params.map do |task_name, options|
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
- return build_docker_commands(docker_params, task_name: task_name) if docker_params
47
- build_docker_compose_commands(docker_compose_params) if docker_compose_params
48
- end
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
- def version_supported?(version)
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