From 556d1d13ad6ce056fb52d920e36c08192c7103c5 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 16 Feb 2025 23:08:46 +0200 Subject: change foo[...] to foo(...) syntax for deps test deps --- lib/dsl.rb | 2 +- lib/dslkeywords/keyword.rb | 2 +- lib/dslkeywords/resource.rb | 20 ++++++++------------ test/lib/dslkeywords/dependency_test.rb | 25 +++++++++++++++++++------ 4 files changed, 29 insertions(+), 20 deletions(-) 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/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/resource.rb b/lib/dslkeywords/resource.rb index c9098d4..18239fc 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -13,31 +13,27 @@ module RCM 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| + @depends_on = {} if @depends_on.nil? + 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?(other) = @depends_on.nil? ? false : @depends_on.key?(other) end # A resource is something concrete to be managed, e.g. a file, or a CRON job. diff --git a/test/lib/dslkeywords/dependency_test.rb b/test/lib/dslkeywords/dependency_test.rb index 324a756..393552f 100644 --- a/test/lib/dslkeywords/dependency_test.rb +++ b/test/lib/dslkeywords/dependency_test.rb @@ -5,25 +5,38 @@ 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.keys.length + assert foo.depends_on?("notify('bar')") + assert foo.depends_on?("notify('baz')") + + assert_equal 0, bar.depends_on.keys.length + + assert_equal 1, baz.depends_on.keys.length + 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 -- cgit v1.2.3 From a246ade229a2a1578358eb25fe5855bc5cf22732 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 16 Feb 2025 23:15:45 +0200 Subject: sugar --- lib/dslkeywords/resource.rb | 6 +++++- playground/Rakefile | 2 +- test/lib/dslkeywords/dependency_test.rb | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/dslkeywords/resource.rb b/lib/dslkeywords/resource.rb index 18239fc..4c28809 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -33,7 +33,11 @@ module RCM end end - def depends_on?(other) = @depends_on.nil? ? false : @depends_on.key?(other) + def depends_on?(*others) + return false if @depends_on.nil? + + others.flatten.none? { |other| !@depends_on.key?(other) } + end end # A resource is something concrete to be managed, e.g. a file, or a CRON job. 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 393552f..4a71485 100644 --- a/test/lib/dslkeywords/dependency_test.rb +++ b/test/lib/dslkeywords/dependency_test.rb @@ -24,8 +24,7 @@ class RCMDependencyTest < Minitest::Test end assert_equal 2, foo.depends_on.keys.length - assert foo.depends_on?("notify('bar')") - assert foo.depends_on?("notify('baz')") + assert foo.depends_on?("notify('bar')", "notify('baz')") assert_equal 0, bar.depends_on.keys.length -- cgit v1.2.3 From d64135e44904500d65b029fb31216a52d507a7be Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 16 Feb 2025 23:49:20 +0200 Subject: can find resource objs --- lib/dslkeywords/file.rb | 1 + lib/dslkeywords/notify.rb | 6 +++++- lib/dslkeywords/resource.rb | 31 +++++++++++++++++++++++++++---- test/lib/dslkeywords/dependency_test.rb | 9 +++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/dslkeywords/file.rb b/lib/dslkeywords/file.rb index 501b7c8..b42f223 100644 --- a/lib/dslkeywords/file.rb +++ b/lib/dslkeywords/file.rb @@ -25,6 +25,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) 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 4c28809..1634061 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -29,19 +29,42 @@ module RCM others.flatten.each do |other| info "Registered dependency on #{other}" - @depends_on[other] = {} + @depends_on[other] = nil end end - def depends_on?(*others) - return false if @depends_on.nil? + def depends_on?(*others) = others.flatten.none? { |other| !@depends_on&.key?(other) } + end + + # To resolve dependencies + module DependencyEvaluator + attr_reader :evaluated + + def evaluate! + return false if @evaluated + + @depends_on = {} if @depends_on.nil? + @depends_on.each_key do |id| + dependency = Resource.find(id) + end - others.flatten.none? { |other| !@depends_on.key?(other) } + @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 + + def self.find(id) + klass = Object.const_get("RCM::#{id.split('(').first.capitalize}") + resource = ObjectSpace.each_object(klass).find { |obj| obj.id == id } + raise NoSuchResourceObject, "Unable to find resource #{id}" if resource.nil? + + resource + end end end diff --git a/test/lib/dslkeywords/dependency_test.rb b/test/lib/dslkeywords/dependency_test.rb index 4a71485..e9bc205 100644 --- a/test/lib/dslkeywords/dependency_test.rb +++ b/test/lib/dslkeywords/dependency_test.rb @@ -27,6 +27,7 @@ class RCMDependencyTest < Minitest::Test assert foo.depends_on?("notify('bar')", "notify('baz')") assert_equal 0, bar.depends_on.keys.length + refute bar.depends_on?('foo') assert_equal 1, baz.depends_on.keys.length assert baz.depends_on?("notify('bar')") @@ -39,4 +40,12 @@ class RCMDependencyTest < Minitest::Test 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 end -- cgit v1.2.3 From ed8cf0c0c285abab24479b7af3bc73f2c7340822 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 17 Feb 2025 00:12:20 +0200 Subject: can detect dependency loop --- lib/dslkeywords/resource.rb | 19 +++++++++++++++---- test/lib/dslkeywords/dependency_test.rb | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/dslkeywords/resource.rb b/lib/dslkeywords/resource.rb index 1634061..2320077 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -40,14 +40,25 @@ module RCM 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 @depends_on = {} if @depends_on.nil? - @depends_on.each_key do |id| - dependency = Resource.find(id) - end + # Try to evaluate all dependencies recursively. + @depends_on.each_key.map { Resource.find(_1) }.each(&:evaluate!) + + # Raise an exception when there are still unresolved dependencies. + unresolved = @depends_on.each_key.map { Resource.find(_1) }.reject(&:evaluated) + raise UnresolvedDependency, "Unresolved dependencies: #{unresolved.map(&:id)}" if unresolved.count.positive? + + @loop_detection = false @evaluated = true end end @@ -61,7 +72,7 @@ module RCM def self.find(id) klass = Object.const_get("RCM::#{id.split('(').first.capitalize}") - resource = ObjectSpace.each_object(klass).find { |obj| obj.id == id } + resource = ObjectSpace.each_object(klass).find { _1.id == id } raise NoSuchResourceObject, "Unable to find resource #{id}" if resource.nil? resource diff --git a/test/lib/dslkeywords/dependency_test.rb b/test/lib/dslkeywords/dependency_test.rb index e9bc205..f1280eb 100644 --- a/test/lib/dslkeywords/dependency_test.rb +++ b/test/lib/dslkeywords/dependency_test.rb @@ -48,4 +48,21 @@ class RCMDependencyTest < Minitest::Test 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 end -- cgit v1.2.3 From a8d7029846faff9db5a99dbe011b3765a1350ee4 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 17 Feb 2025 00:16:47 +0200 Subject: sugar --- TODO.md | 1 - lib/dslkeywords/resource.rb | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index ad66ca3..1f83cad 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/dslkeywords/resource.rb b/lib/dslkeywords/resource.rb index 2320077..29824e9 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -7,6 +7,7 @@ module RCM module ResourceDependencies def initialize(...) super(...) + @depends_on = {} @valid_resources = Set.new ObjectSpace.each_object(Class).each do |klass| @valid_resources << klass.to_s.sub('RCM::', '').downcase.to_sym if klass < Resource @@ -24,7 +25,6 @@ module RCM def respond_to_missing? = true def depends_on(*others) - @depends_on = {} if @depends_on.nil? return @depends_on if others.empty? others.flatten.each do |other| @@ -49,7 +49,6 @@ module RCM raise DependencyLoop, "Dependency loop detected for #{id}" if @loop_detection @loop_detection = true - @depends_on = {} if @depends_on.nil? # Try to evaluate all dependencies recursively. @depends_on.each_key.map { Resource.find(_1) }.each(&:evaluate!) -- cgit v1.2.3 From e13d1a0a7df840bd86e68f6b9604c764c54cc60f Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 17 Feb 2025 00:20:47 +0200 Subject: use a set --- lib/dslkeywords/resource.rb | 10 +++++----- test/lib/dslkeywords/dependency_test.rb | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/dslkeywords/resource.rb b/lib/dslkeywords/resource.rb index 29824e9..11df70e 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -7,7 +7,7 @@ module RCM module ResourceDependencies def initialize(...) super(...) - @depends_on = {} + @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 @@ -29,11 +29,11 @@ module RCM others.flatten.each do |other| info "Registered dependency on #{other}" - @depends_on[other] = nil + @depends_on << other end end - def depends_on?(*others) = others.flatten.none? { |other| !@depends_on&.key?(other) } + def depends_on?(*others) = others.flatten.none? { |other| !@depends_on&.include?(other) } end # To resolve dependencies @@ -51,10 +51,10 @@ module RCM @loop_detection = true # Try to evaluate all dependencies recursively. - @depends_on.each_key.map { Resource.find(_1) }.each(&:evaluate!) + @depends_on.each.map { Resource.find(_1) }.each(&:evaluate!) # Raise an exception when there are still unresolved dependencies. - unresolved = @depends_on.each_key.map { Resource.find(_1) }.reject(&:evaluated) + unresolved = @depends_on.each.map { Resource.find(_1) }.reject(&:evaluated) raise UnresolvedDependency, "Unresolved dependencies: #{unresolved.map(&:id)}" if unresolved.count.positive? @loop_detection = false diff --git a/test/lib/dslkeywords/dependency_test.rb b/test/lib/dslkeywords/dependency_test.rb index f1280eb..aeb2790 100644 --- a/test/lib/dslkeywords/dependency_test.rb +++ b/test/lib/dslkeywords/dependency_test.rb @@ -23,13 +23,13 @@ class RCMDependencyTest < Minitest::Test end end - assert_equal 2, foo.depends_on.keys.length + assert_equal 2, foo.depends_on.count assert foo.depends_on?("notify('bar')", "notify('baz')") - assert_equal 0, bar.depends_on.keys.length + assert_equal 0, bar.depends_on.count refute bar.depends_on?('foo') - assert_equal 1, baz.depends_on.keys.length + assert_equal 1, baz.depends_on.count assert baz.depends_on?("notify('bar')") end -- cgit v1.2.3 From 9bb314cb170a651571b57767d33b3c01112aa5bc Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 17 Feb 2025 00:26:07 +0200 Subject: cache the resource objs --- lib/dslkeywords/resource.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/dslkeywords/resource.rb b/lib/dslkeywords/resource.rb index 11df70e..f646030 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -69,12 +69,16 @@ module RCM class NoSuchResourceObject < StandardError; end + @@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 + @@resource_find_cache[id] = resource end end end -- cgit v1.2.3 From 614a3b5062d3a37d7f1eb59ad47e4ec5d791edc6 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 18 Feb 2025 10:46:44 +0200 Subject: backupping file when change --- TODO.md | 1 - lib/dslkeywords/file.rb | 22 +++++++++++++++++++++- lib/dslkeywords/resource.rb | 2 ++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 1f83cad..120fec9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,6 @@ # TODO * Recursively install a directory -* Backup of files when changed * Support for file deletion * Support for file modes (owner, chmod) * Support for symlinks diff --git a/lib/dslkeywords/file.rb b/lib/dslkeywords/file.rb index b42f223..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 @@ -45,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/resource.rb b/lib/dslkeywords/resource.rb index f646030..f665a50 100644 --- a/lib/dslkeywords/resource.rb +++ b/lib/dslkeywords/resource.rb @@ -69,6 +69,8 @@ module RCM class NoSuchResourceObject < StandardError; end + # TODO: Detect duplicate resource definition + @@resource_find_cache = {} def self.find(id) -- cgit v1.2.3