djin 0.8.0 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.
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