summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.md1
-rw-r--r--lib/dsl.rb2
-rw-r--r--lib/dslkeywords/file.rb23
-rw-r--r--lib/dslkeywords/keyword.rb2
-rw-r--r--lib/dslkeywords/notify.rb6
-rw-r--r--lib/dslkeywords/resource.rb63
-rw-r--r--playground/Rakefile2
-rw-r--r--test/lib/dslkeywords/dependency_test.rb50
8 files changed, 125 insertions, 24 deletions
diff --git a/TODO.md b/TODO.md
index 4424c9e..9304832 100644
--- a/TODO.md
+++ b/TODO.md
@@ -16,7 +16,6 @@
* Binary installation support (file copy from source to dest)
* Local file system
* scp? git? http/s?
-* Object dependencies / ensuring correct order of evaluation
* Manage services (start, stop, restart (subscribe))
* Manage packages (install, update, deinstall)
* Fedora
diff --git a/lib/dsl.rb b/lib/dsl.rb
index caed790..b9062e5 100644
--- a/lib/dsl.rb
+++ b/lib/dsl.rb
@@ -25,7 +25,7 @@ module RCM
def initialize(reset)
DSL.reset! if reset
- @id = "dsl[#{@@rcm_counter += 1}]"
+ @id = "dsl(#{@@rcm_counter += 1})"
@conds_met = true
@scheduled = []
yield self if block_given?
diff --git a/lib/dslkeywords/file.rb b/lib/dslkeywords/file.rb
index 501b7c8..e83906e 100644
--- a/lib/dslkeywords/file.rb
+++ b/lib/dslkeywords/file.rb
@@ -1,13 +1,32 @@
+require 'digest'
require 'erb'
require 'fileutils'
require_relative 'resource'
module RCM
+ # Backup the file on change
+ module FileBackup
+ def backup!(path)
+ return unless ::File.exist?(path)
+
+ backup_dir = "#{::File.dirname(path)}/.rcm"
+ Dir.mkdir(backup_dir) unless ::File.directory?(backup_dir)
+ checksum = Digest::SHA256.file(path).hexdigest
+ backup_path = "#{backup_dir}/#{::File.basename(path)}.#{checksum}"
+ return if ::File.exist?(backup_path)
+
+ info("Backing up #{path} -> #{backup_path}")
+ ::File.rename(path, backup_path)
+ end
+ end
+
# Managing files
class File < Resource
attr_reader :path
+ include FileBackup
+
def initialize(path)
super(path)
@path = path
@@ -25,6 +44,7 @@ module RCM
def ensure_line(line) = @ensure_line = line
def evaluate!
+ return unless super
return evaluate_ensure_line! unless @ensure_line.nil?
write_content!(real_content)
@@ -44,9 +64,10 @@ module RCM
def write_content!(text)
create_parent_directory!
debug text if option :debug
- info "Creating file #{@path}"
+ info "Managing file #{@path}"
tmp_path = "#{@path}.tmp"
::File.write(tmp_path, text)
+ backup!(@path)
::File.rename(tmp_path, @path)
end
diff --git a/lib/dslkeywords/keyword.rb b/lib/dslkeywords/keyword.rb
index 2696e41..2dfff91 100644
--- a/lib/dslkeywords/keyword.rb
+++ b/lib/dslkeywords/keyword.rb
@@ -9,7 +9,7 @@ module RCM
include Options
include Log
- def initialize(name) = @id = "#{self.class.to_s.sub('RCM::', '').downcase}['#{name}']"
+ def initialize(name) = @id = "#{self.class.to_s.sub('RCM::', '').downcase}('#{name}')"
def to_s = @id
end
end
diff --git a/lib/dslkeywords/notify.rb b/lib/dslkeywords/notify.rb
index 602d216..e1d8a3d 100644
--- a/lib/dslkeywords/notify.rb
+++ b/lib/dslkeywords/notify.rb
@@ -15,7 +15,11 @@ module RCM
@message = msg unless msg.nil?
end
- def evaluate! = puts "#{id} => #{@message}"
+ def evaluate!
+ return unless super
+
+ puts "#{id} => #{@message}"
+ end
end
# Add notify keyword to the DSL
diff --git a/lib/dslkeywords/resource.rb b/lib/dslkeywords/resource.rb
index c9098d4..f665a50 100644
--- a/lib/dslkeywords/resource.rb
+++ b/lib/dslkeywords/resource.rb
@@ -7,41 +7,80 @@ module RCM
module ResourceDependencies
def initialize(...)
super(...)
+ @depends_on = Set.new
@valid_resources = Set.new
ObjectSpace.each_object(Class).each do |klass|
@valid_resources << klass.to_s.sub('RCM::', '').downcase.to_sym if klass < Resource
end
end
- # Only to have the resourcename[id] syntax available in the DSL
- class SyntaxSugar
- def initialize(name) = @name = name
- def [](other) = "#{@name}['#{other}']"
- end
-
class NoSuchResourceType < StandardError; end
- def method_missing(method_name)
+ def method_missing(method_name, *args)
raise NoSuchResourceType, "No such resource type: #{method_name}" unless @valid_resources.include?(method_name)
- SyntaxSugar.new(method_name)
+ args.map { |arg| "#{method_name}('#{arg}')" }
end
def respond_to_missing? = true
def depends_on(*others)
- @dependencies = {} if @dependencies.nil?
- others.each do |other|
+ return @depends_on if others.empty?
+
+ others.flatten.each do |other|
info "Registered dependency on #{other}"
- @dependencies[other] = {}
+ @depends_on << other
end
end
- def dependencies = @dependencies.nil ? [] : @dependencies
+ def depends_on?(*others) = others.flatten.none? { |other| !@depends_on&.include?(other) }
+ end
+
+ # To resolve dependencies
+ module DependencyEvaluator
+ attr_reader :evaluated
+
+ class DependencyLoop < StandardError; end
+ class UnresolvedDependency < StandardError; end
+
+ def evaluate!
+ return false if @evaluated
+
+ raise DependencyLoop, "Dependency loop detected for #{id}" if @loop_detection
+
+ @loop_detection = true
+
+ # Try to evaluate all dependencies recursively.
+ @depends_on.each.map { Resource.find(_1) }.each(&:evaluate!)
+
+ # Raise an exception when there are still unresolved dependencies.
+ unresolved = @depends_on.each.map { Resource.find(_1) }.reject(&:evaluated)
+ raise UnresolvedDependency, "Unresolved dependencies: #{unresolved.map(&:id)}" if unresolved.count.positive?
+
+ @loop_detection = false
+ @evaluated = true
+ end
end
# A resource is something concrete to be managed, e.g. a file, or a CRON job.
class Resource < Keyword
+ include DependencyEvaluator
include ResourceDependencies
+
+ class NoSuchResourceObject < StandardError; end
+
+ # TODO: Detect duplicate resource definition
+
+ @@resource_find_cache = {}
+
+ def self.find(id)
+ return @@resource_find_cache[id] if @@resource_find_cache.key?(id)
+
+ klass = Object.const_get("RCM::#{id.split('(').first.capitalize}")
+ resource = ObjectSpace.each_object(klass).find { _1.id == id }
+ raise NoSuchResourceObject, "Unable to find resource #{id}" if resource.nil?
+
+ @@resource_find_cache[id] = resource
+ end
end
end
diff --git a/playground/Rakefile b/playground/Rakefile
index ef53d00..35ae811 100644
--- a/playground/Rakefile
+++ b/playground/Rakefile
@@ -37,7 +37,7 @@ task :foo do
configure do
file '/tmp/foo.txt' do
ensure_line 'foo'
- depends_on file['/tmp/bar.txt']
+ depends_on file '/tmp/bar.txt'
end
file '/tmp/bar.txt' do
diff --git a/test/lib/dslkeywords/dependency_test.rb b/test/lib/dslkeywords/dependency_test.rb
index 324a756..aeb2790 100644
--- a/test/lib/dslkeywords/dependency_test.rb
+++ b/test/lib/dslkeywords/dependency_test.rb
@@ -5,25 +5,63 @@ require_relative '../../../lib/dsl'
class RCMDependencyTest < Minitest::Test
def test_depends_on
+ foo = nil
+ bar = nil
+ baz = nil
+
configure_from_scratch do
- notify 'foo' do
- depends_on notify['bar'], notify['baz']
+ foo = notify 'foo' do
+ depends_on notify 'bar', 'baz'
:foo_message
end
- notify 'bar'
+ bar = notify 'bar'
- notify 'baz' do
- depends_on notify['bar']
+ baz = notify 'baz' do
+ depends_on notify 'bar'
:baz_message
end
end
+
+ assert_equal 2, foo.depends_on.count
+ assert foo.depends_on?("notify('bar')", "notify('baz')")
+
+ assert_equal 0, bar.depends_on.count
+ refute bar.depends_on?('foo')
+
+ assert_equal 1, baz.depends_on.count
+ assert baz.depends_on?("notify('bar')")
end
def test_depends_on_invalid_resource
assert_raises(RCM::ResourceDependencies::NoSuchResourceType) do
configure_from_scratch do
- notify { depends_on invalid['baz'] }
+ notify { depends_on invalid('baz') }
+ end
+ end
+ end
+
+ def test_depends_on_non_existant_dependency
+ assert_raises(RCM::Resource::NoSuchResourceObject) do
+ configure_from_scratch do
+ notify { depends_on notify('nonexistant') }
+ end
+ end
+ end
+
+ def test_dependency_loop
+ assert_raises(RCM::DependencyEvaluator::DependencyLoop) do
+ configure_from_scratch do
+ notify('loop') { depends_on notify('loop') }
+ end
+ end
+ end
+
+ def test_dependency_loop_indirect
+ assert_raises(RCM::DependencyEvaluator::DependencyLoop) do
+ configure_from_scratch do
+ notify('loop') { depends_on notify('pool') }
+ notify('pool') { depends_on notify('loop') }
end
end
end