djin 0.8.0 → 0.11.2

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/Vertofile CHANGED
@@ -1,4 +1,4 @@
1
- verto_version '0.8.0'
1
+ verto_version '0.10.0'
2
2
 
3
3
  config {
4
4
  version.prefix = 'v' # Adds a version_prefix
@@ -12,28 +12,21 @@ context(branch('master')) {
12
12
  }
13
13
 
14
14
  before_tag_creation {
15
- version_changes = sh(
16
- %q#git log --oneline --decorate | grep -B 100 -m 1 "tag:" | grep "pull request" | awk '{print $1}' | xargs git show --format='%b' | grep -v Approved | grep -v "^$" | grep -E "^[[:space:]]*\[.*\]" | sed 's/^[[:space:]]*\(.*\)/ * \1/'#, output: false
17
- ).output
18
-
19
- puts "---------------------------"
20
- version_changes = "## #{new_version} - #{Time.now.strftime('%d/%m/%Y')}\n#{version_changes}\n"
21
- exit unless confirm("Create new Realease?\n" \
22
- "---------------------------\n" \
23
- "#{version_changes}" \
24
- "---------------------------\n"
25
- )
26
-
27
- # CHANGELOG
28
- file('CHANGELOG.md').prepend(version_changes)
15
+ update_changelog(with: :merged_pull_requests_with_bracketed_labels,
16
+ confirmation: true,
17
+ filename: 'CHANGELOG.md')
18
+
29
19
  git!('add CHANGELOG.md')
30
20
 
31
- file('lib/djin/version.rb').replace(latest_version.to_s, new_version.to_s)
32
- file('djin.yml').replace(latest_version.to_s, new_version.to_s)
33
- file('examples/djin.yml').replace(latest_version.to_s, new_version.to_s)
21
+ files_to_change_version_once = %w[lib/djin/version.rb djin.yml] + Dir['examples/**/*.yml'] + Dir['spec/support/fixtures/**/*.yml']
22
+
23
+ files_to_change_version_once.each do |filename|
24
+ file(filename).replace(latest_version.to_s, new_version.to_s)
25
+ end
26
+
34
27
  file('README.md').replace_all(latest_version.to_s, new_version.to_s)
35
28
 
36
- git!('add lib/djin/version.rb djin.yml examples/djin.yml README.md')
29
+ git!("add #{files_to_change_version_once.join(' ')} README.md")
37
30
 
38
31
  sh!('bundle install')
39
32
  sh!('rake install')
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  # Specify which files should be added to the gem when it is released.
22
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
23
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|docker)/}) }
25
25
  end
26
26
  spec.bindir = 'exe'
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency 'dry-equalizer', '~> 0.3.0'
32
32
  spec.add_dependency 'dry-struct', '~> 1.3.0'
33
33
  spec.add_dependency 'dry-validation', '~> 1.5.1'
34
+ spec.add_dependency 'git', '~> 1.8.1'
34
35
  spec.add_dependency 'mustache', '~> 1.1.1'
35
36
  spec.add_dependency 'vseries', '~> 0.1.0'
36
37
  spec.add_development_dependency 'bundler', '~> 2.0'
@@ -38,4 +39,5 @@ Gem::Specification.new do |spec|
38
39
  spec.add_development_dependency 'rake', '~> 13.0'
39
40
  spec.add_development_dependency 'rspec', '~> 3.0'
40
41
  spec.add_development_dependency 'rubocop'
42
+ spec.add_development_dependency 'simplecov', '~> 0.17.0'
41
43
  end
data/djin.yml CHANGED
@@ -1,4 +1,4 @@
1
- djin_version: '0.8.0'
1
+ djin_version: '0.11.2'
2
2
 
3
3
  _default_run_options: &default_run_options
4
4
  options: "--rm --entrypoint=''"
@@ -9,11 +9,21 @@ tasks:
9
9
  docker-compose:
10
10
  service: app
11
11
  run:
12
- commands: "cd /usr/src/djin && rspec {{args}}"
12
+ commands: "rspec {{args}}"
13
13
  <<: *default_run_options
14
14
  aliases:
15
15
  - rspec
16
16
 
17
+ lint:
18
+ description: Lint
19
+ docker-compose:
20
+ service: app
21
+ run:
22
+ commands: "rubocop {{args}}"
23
+ <<: *default_run_options
24
+ aliases:
25
+ - rubocop
26
+
17
27
  sh:
18
28
  description: Enter app service shell
19
29
  docker-compose:
@@ -31,5 +41,6 @@ tasks:
31
41
  release:
32
42
  local:
33
43
  run:
44
+ - (source ~/.zshrc || true)
34
45
  - verto tag up {{args}}
35
46
  - bundle exec rake release
@@ -1,6 +1,20 @@
1
- version: '3'
1
+ version: "3.9"
2
+
2
3
  services:
3
4
  app:
4
- build: .
5
+ build:
6
+ context: .
7
+ target: dev
8
+ entrypoint: 'sh docker-entrypoint.sh'
9
+ command: 'djin'
10
+ tty: true
11
+ stdin_open: true
5
12
  volumes:
6
13
  - .:/usr/src/djin
14
+ depends_on:
15
+ - gitserver
16
+
17
+ gitserver:
18
+ image: catks/gitserver-http:0.1.0
19
+ volumes:
20
+ - ./docker/git_server/repos/:/var/lib/initial/
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ bundle check || bundle install
6
+
7
+ exec bundle exec "$@"
@@ -1,5 +1,17 @@
1
1
  ---
2
- djin_version: '0.8.0'
2
+ djin_version: '0.11.2'
3
+
4
+ include:
5
+ - file: 'djin_lib/test.yml'
6
+ context:
7
+ variables:
8
+ namespace: 'test:'
9
+
10
+ - file: 'djin_lib/test.yml'
11
+ context:
12
+ variables:
13
+ namespace: 'test2:'
14
+
3
15
 
4
16
  tasks:
5
17
  default:
@@ -7,12 +19,6 @@ tasks:
7
19
  image: "ruby:2.5"
8
20
  run:
9
21
  - "ruby -e 'puts \\\" Hello\\\"'"
10
- test:
11
- docker-compose:
12
- service: app
13
- run:
14
- commands: rspec
15
- options: "--rm"
16
22
 
17
23
  script:
18
24
  docker:
@@ -0,0 +1,12 @@
1
+ djin_version: '0.11.2'
2
+
3
+ _default_run_options: &default_run_options
4
+ options: "--rm --entrypoint=''"
5
+
6
+ tasks:
7
+ "{{namespace}}unit":
8
+ docker-compose:
9
+ service: app
10
+ run:
11
+ commands: "cd /usr/src/djin && rspec {{args}}"
12
+ <<: *default_run_options
@@ -0,0 +1,17 @@
1
+ djin_version: '0.8.0'
2
+
3
+ tasks:
4
+ "{{namespace}}:ssh":
5
+ local:
6
+ run:
7
+ - ssh {{ssh_user}}@{{host}}
8
+
9
+ "{{namespace}}:restart":
10
+ local:
11
+ run:
12
+ - ssh -t {{ssh_user}}@{{host}} restart
13
+
14
+ "{{namespace}}:logs":
15
+ local:
16
+ run:
17
+ - ssh -t {{ssh_user}}@{{host}} tail -f /var/log/my_log
@@ -0,0 +1,22 @@
1
+ djin_version: '0.11.2'
2
+
3
+ include:
4
+ - file: '.djin/server_tasks.yml'
5
+ context:
6
+ variables:
7
+ namespace: host1
8
+ host: host1.com
9
+ ssh_user: my_user
10
+
11
+ - file: '.djin/server_tasks.yml'
12
+ context:
13
+ variables:
14
+ namespace: host2
15
+ host: host2.com
16
+ ssh_user: my_user
17
+
18
+ tasks:
19
+ hello_command:
20
+ local:
21
+ run:
22
+ - echo 'Hello Djin'
@@ -0,0 +1,9 @@
1
+ djin_version: '0.11.2'
2
+
3
+ include:
4
+ - git: 'https://gitserver/myrepo.git'
5
+ version: 'master'
6
+ file: 'examples/djin_lib/test.yml'
7
+ context:
8
+ variables:
9
+ namespace: 'remote:'
@@ -8,44 +8,94 @@ require 'dry-validation'
8
8
  require 'vseries'
9
9
  require 'dry/cli'
10
10
  require 'mustache'
11
+ require 'optparse'
12
+ require 'git'
11
13
  require 'djin/extensions/hash_extensions'
14
+ require 'djin/extensions/object_extensions'
12
15
  require 'djin/entities/types'
13
16
  require 'djin/entities/task'
14
- require 'djin/entities/file_config'
17
+ require 'djin/entities/include_config.rb'
18
+ require 'djin/entities/main_config'
15
19
  require 'djin/interpreter/base_command_builder'
16
20
  require 'djin/interpreter/docker_command_builder'
17
21
  require 'djin/interpreter/docker_compose_command_builder'
18
22
  require 'djin/interpreter/local_command_builder'
19
23
  require 'djin/interpreter'
24
+ require 'djin/include_resolver'
20
25
  require 'djin/config_loader'
21
26
  require 'djin/executor'
27
+ require 'djin/root_cli_parser'
22
28
  require 'djin/cli'
23
29
  require 'djin/task_contract'
24
30
  require 'djin/repositories/task_repository'
31
+ require 'djin/repositories/remote_config_repository'
32
+ require 'djin/memory_cache'
25
33
 
26
34
  module Djin
27
35
  class Error < StandardError; end
28
36
 
29
- def self.load_tasks!(path = Pathname.getwd.join('djin.yml'))
30
- abort 'Error: djin.yml not found' unless path.exist?
37
+ using Djin::ObjectExtensions
31
38
 
32
- file_config = ConfigLoader.load!(path.read)
39
+ class << self
40
+ def load_tasks!(*file_paths)
41
+ files = file_paths.presence || RootCliParser.parse![:files] || ['djin.yml']
33
42
 
34
- # TODO: Make all tasks be under 'tasks' key, passing only the tasks here
35
- tasks = Interpreter.load!(file_config)
43
+ file_config = ConfigLoader.load_files!(*files)
36
44
 
37
- @task_repository = TaskRepository.new(tasks)
38
- CLI.load_tasks!(tasks)
39
- rescue Djin::Interpreter::InvalidConfigurationError => e
40
- error_name = e.class.name.split('::').last
41
- abort("[#{error_name}] #{e.message}")
42
- end
45
+ # TODO: Make all tasks be under 'tasks' key, passing only the tasks here
46
+ tasks = Interpreter.load!(file_config)
43
47
 
44
- def self.tasks
45
- task_repository.all
46
- end
48
+ @task_repository = TaskRepository.new(tasks)
49
+
50
+ remote_configs = file_config.include_configs.select { |f| f.type == :remote }
51
+ @remote_config_repository = RemoteConfigRepository.new(remote_configs)
52
+
53
+ CLI.load_tasks!(tasks)
54
+ rescue Djin::Interpreter::InvalidConfigurationError => e
55
+ error_name = e.class.name.split('::').last
56
+ abort("[#{error_name}] #{e.message}")
57
+ end
58
+
59
+ def tasks
60
+ task_repository.all
61
+ end
62
+
63
+ def task_repository
64
+ @task_repository ||= TaskRepository.new
65
+ end
66
+
67
+ def remote_config_repository
68
+ @remote_config_repository ||= RemoteConfigRepository.new
69
+ end
70
+
71
+ def cache
72
+ @cache ||= MemoryCache.new
73
+ end
74
+
75
+ def root_path
76
+ Pathname.new File.expand_path(__dir__ + '/..')
77
+ end
78
+
79
+ def warn(message, type: 'WARNING')
80
+ stderr.puts "[#{type}] #{message}"
81
+ end
82
+
83
+ def warn_once(message, type: 'WARNING')
84
+ return if warnings.include?(message)
85
+
86
+ warn(message, type: type)
87
+
88
+ warnings << message
89
+ end
90
+
91
+ def stderr
92
+ $stderr
93
+ end
94
+
95
+ private
47
96
 
48
- def self.task_repository
49
- @task_repository ||= TaskRepository.new
97
+ def warnings
98
+ @warnings ||= []
99
+ end
50
100
  end
51
101
  end
@@ -28,6 +28,42 @@ module Djin
28
28
  end
29
29
  end
30
30
 
31
+ class File < Dry::CLI::Command
32
+ desc 'Specify a djin file to load (default: djin.yml)'
33
+ argument :filepath, required: true, desc: 'The file path to load'
34
+
35
+ def call(filename:, **)
36
+ # The actual behaviour is on RootCliParser
37
+ end
38
+ end
39
+
40
+ module RemoteConfig
41
+ class Fetch < Dry::CLI::Command
42
+ desc 'Fetchs missing remote configs'
43
+
44
+ def call(*)
45
+ Djin.remote_config_repository.fetch_all
46
+ end
47
+ end
48
+
49
+ class Clear < Dry::CLI::Command
50
+ desc 'clear downloaded remote configs'
51
+ option :all,
52
+ type: :boolean,
53
+ default: false,
54
+ desc: 'Remove all remote configs, not only the ones referenced in the current djin file'
55
+
56
+ def call(all:)
57
+ return Djin.remote_config_repository.clear_all if all
58
+
59
+ Djin.remote_config_repository.clear
60
+ end
61
+ end
62
+ end
63
+
64
+ register '-f', File, aliases: ['--file']
31
65
  register '--version', Version, aliases: ['-v']
66
+ register 'remote-config fetch', RemoteConfig::Fetch
67
+ register 'remote-config clear', RemoteConfig::Clear
32
68
  end
33
69
  end
@@ -4,20 +4,38 @@ module Djin
4
4
  # TODO: Refactor this class to be the Interpreter
5
5
  # class and use the current interpreter as
6
6
  # a TaskLoader
7
+
8
+ # rubocop:disable Metrics/ClassLength
7
9
  class ConfigLoader
8
10
  using Djin::HashExtensions
9
- RESERVED_WORDS = %w[djin_version variables tasks].freeze
11
+ RESERVED_WORDS = %w[djin_version variables tasks include].freeze
12
+
13
+ # Change Base Error
14
+ FileNotFoundError = Class.new(Interpreter::InvalidConfigurationError)
15
+
16
+ def self.load_files!(*files, runtime_config: {}, base_directory: '.')
17
+ files.map do |file_path|
18
+ ConfigLoader.load!(file_path, runtime_config: runtime_config, base_directory: base_directory)
19
+ end&.reduce(:deep_merge)
20
+ end
10
21
 
11
- def self.load!(template_file)
12
- new(template_file).load!
22
+ def self.load!(template_file_path, runtime_config: {}, base_directory: '.')
23
+ new(template_file_path, runtime_config: runtime_config, base_directory: base_directory).load!
13
24
  end
14
25
 
15
- def initialize(template_file)
16
- @template_file = template_file
26
+ def initialize(template_file_path, runtime_config: {}, base_directory: '.')
27
+ @base_directory = Pathname.new(base_directory)
28
+ @template_file = @base_directory.join(template_file_path)
29
+
30
+ file_not_found!(@template_file) unless @template_file.exist?
31
+
32
+ @template_file_content = Djin.cache.fetch(@template_file.realpath.to_s) { @template_file.read }
33
+ @runtime_config = runtime_config
17
34
  end
18
35
 
19
36
  def load!
20
37
  validate_version!
38
+ validate_missing_config!
21
39
 
22
40
  file_config
23
41
  end
@@ -25,52 +43,38 @@ module Djin
25
43
  private
26
44
 
27
45
  def file_config
28
- FileConfig.new(
46
+ MainConfig.new(
29
47
  djin_version: version,
30
48
  variables: variables,
31
49
  tasks: tasks,
32
- raw_tasks: raw_tasks
50
+ raw_tasks: raw_tasks,
51
+ include_configs: @include_configs || []
33
52
  )
34
53
  end
35
54
 
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
55
  def version
55
56
  # TODO: Deprecates djin_version and use version instead
56
57
  @version || raw_djin_config['djin_version']
57
58
  end
58
59
 
59
60
  def variables
60
- @variables ||= raw_djin_config['variables']&.symbolize_keys || {}
61
+ @variables ||= included_variables.merge(raw_djin_config['variables']&.symbolize_keys || {})
61
62
  end
62
63
 
63
64
  def tasks
64
- rendered_djin_config['tasks'] || legacy_tasks
65
+ included_tasks.merge(rendered_djin_config['tasks'] || legacy_tasks)
65
66
  end
66
67
 
67
68
  def raw_tasks
68
- raw_djin_config['tasks'] || legacy_raw_tasks
69
+ included_raw_tasks.merge(raw_djin_config['tasks'] || legacy_raw_tasks)
69
70
  end
70
71
 
71
72
  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'
73
+ Djin.warn_once(
74
+ 'Root tasks are deprecated and will be removed in Djin 1.0.0,' \
75
+ ' put the tasks under \'tasks\' keyword',
76
+ type: 'DEPRECATED'
77
+ )
74
78
 
75
79
  rendered_djin_config.except(*RESERVED_WORDS).reject { |task| task.start_with?('_') }
76
80
  end
@@ -79,6 +83,57 @@ module Djin
79
83
  raw_djin_config.except(*RESERVED_WORDS).reject { |task| task.start_with?('_') }
80
84
  end
81
85
 
86
+ def included_variables
87
+ return {} unless included_config
88
+
89
+ included_config.variables
90
+ end
91
+
92
+ def included_tasks
93
+ return {} unless included_config
94
+
95
+ included_config.tasks
96
+ end
97
+
98
+ def included_raw_tasks
99
+ return {} unless included_config
100
+
101
+ included_config.raw_tasks
102
+ end
103
+
104
+ # TODO: Rename method
105
+ def included_config
106
+ @included_config ||= begin
107
+ present_include_configs&.map do |present_include|
108
+ ConfigLoader.load!(present_include.file, base_directory: @template_file.dirname,
109
+ # TODO: Rename to context_config
110
+ runtime_config: present_include.context)
111
+ end&.reduce(:deep_merge)
112
+ end
113
+ end
114
+
115
+ def present_include_configs
116
+ include_configs&.select(&:present?)
117
+ end
118
+
119
+ def missing_include_configs
120
+ include_configs&.select(&:missing?)
121
+ end
122
+
123
+ # TODO: Refactor to move include methods to a specific IncludeConfigLoader, maybe rename IncludeResolver
124
+ def include_configs
125
+ @include_configs ||= begin
126
+ # TODO: Rename the resolved variables
127
+ resolver = IncludeResolver.new(base_directory: @template_file.dirname)
128
+
129
+ include_djin_config = raw_djin_config['include'] || []
130
+
131
+ include_djin_config.map do |include_config|
132
+ resolver.call(include_config)
133
+ end
134
+ end
135
+ end
136
+
82
137
  def args
83
138
  index = ARGV.index('--')
84
139
 
@@ -91,6 +146,24 @@ module Djin
91
146
  @env ||= ENV.to_h.symbolize_keys
92
147
  end
93
148
 
149
+ def raw_djin_config
150
+ @raw_djin_config ||= yaml_load(@template_file_content).deep_merge(@runtime_config)
151
+ rescue Psych::SyntaxError => e
152
+ raise Interpreter::InvalidConfigFileError, "File: #{@template_file.realpath}\n #{e.message}"
153
+ end
154
+
155
+ def rendered_djin_config
156
+ @rendered_djin_config ||= begin
157
+ locals = env.merge(variables)
158
+
159
+ rendered_yaml = Mustache.render(@template_file_content,
160
+ args: args.join(' '),
161
+ args?: args.any?,
162
+ **locals)
163
+ yaml_load(rendered_yaml).merge(@runtime_config)
164
+ end
165
+ end
166
+
94
167
  def yaml_load(text)
95
168
  YAML.safe_load(text, [], [], true)
96
169
  end
@@ -102,5 +175,26 @@ module Djin
102
175
 
103
176
  raise Interpreter::VersionNotSupportedError, "Version #{version} is not supported, use #{Djin::VERSION} or higher"
104
177
  end
178
+
179
+ def validate_missing_config!
180
+ missing_include_configs.each do |ic|
181
+ file_not_found!(ic.full_path) if ic.type == :local
182
+
183
+ missing_file_remote_error = "#{ic.git} exists but is missing %s," \
184
+ 'if the file exists in upstream run djin remote-config fetch to fix'
185
+
186
+ file_not_found!(ic.full_path, missing_file_remote_error) if ic.type == :remote && ic.repository_fetched?
187
+
188
+ if ic.type == :remote
189
+ Djin.warn_once "Missing #{ic.git} with version '#{ic.version}', " \
190
+ 'run `djin remote-config fetch` to fetch the config'
191
+ end
192
+ end
193
+ end
194
+
195
+ def file_not_found!(filename, message = "File '%s' not found")
196
+ raise FileNotFoundError, message % filename
197
+ end
105
198
  end
199
+ # rubocop:enable Metrics/ClassLength
106
200
  end