diff options
| author | Paul Buetow <paul@buetow.org> | 2025-02-18 12:26:34 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-02-18 12:26:34 +0200 |
| commit | deda72cb08e1177b15a5506c20fa75edeb876eb6 (patch) | |
| tree | 75ff4767bd096bfc21a065adc5bd1269a47809ff /lib/dslkeywords | |
| parent | 703fd14764974ba1513c987dcb461e4a23d633e1 (diff) | |
| parent | 614a3b5062d3a37d7f1eb59ad47e4ec5d791edc6 (diff) | |
Merge branch 'main' of codeberg.org:snonux/rcm
Diffstat (limited to 'lib/dslkeywords')
| -rw-r--r-- | lib/dslkeywords/file.rb | 23 | ||||
| -rw-r--r-- | lib/dslkeywords/keyword.rb | 2 | ||||
| -rw-r--r-- | lib/dslkeywords/notify.rb | 6 | ||||
| -rw-r--r-- | lib/dslkeywords/resource.rb | 63 |
4 files changed, 79 insertions, 15 deletions
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 |
