From da63b4d8a020bd101ca853585978828e10bedfd0 Mon Sep 17 00:00:00 2001 From: Justin Stoller Date: Mon, 12 Apr 2021 17:10:27 -0700 Subject: [PATCH 1/3] (CODEMGMT-1415) SPIKE assume modules unchanged locally --- lib/r10k/environment/base.rb | 10 ++ lib/r10k/mock_module.rb | 65 ++++++++++++ lib/r10k/puppetfile.rb | 22 +++- lib/r10k/stub_puppetfile.rb | 191 +++++++++++++++++++++++++++++++++++ 4 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 lib/r10k/mock_module.rb create mode 100644 lib/r10k/stub_puppetfile.rb diff --git a/lib/r10k/environment/base.rb b/lib/r10k/environment/base.rb index c54e5752d..eb4193a95 100644 --- a/lib/r10k/environment/base.rb +++ b/lib/r10k/environment/base.rb @@ -1,4 +1,5 @@ require 'r10k/util/subprocess' +require 'r10k/stub_puppetfile' # This class defines a common interface for environment implementations. # @@ -48,8 +49,17 @@ def initialize(name, basedir, dirname, options = {}) @full_path = File.join(@basedir, @dirname) @path = Pathname.new(File.join(@basedir, @dirname)) + if File.exist?(File.join(@full_path, @puppetfile_name || 'Puppetfile')) + @previous_puppetfile = R10K::StubPuppetfile.new(@full_path, nil, nil, @puppetfile_name) + else + logger.debug _("Could not find Puppetfile at: %{path}" % { path: File.join(@full_path, @puppetfile_name || 'Puppetfile') }) + end + + @previous_puppetfile.load if @previous_puppetfile + @puppetfile = R10K::Puppetfile.new(@full_path, nil, nil, @puppetfile_name) @puppetfile.environment = self + @puppetfile.previous_version = @previous_puppetfile end # Synchronize the given environment. diff --git a/lib/r10k/mock_module.rb b/lib/r10k/mock_module.rb new file mode 100644 index 000000000..ce2200d05 --- /dev/null +++ b/lib/r10k/mock_module.rb @@ -0,0 +1,65 @@ +require 'puppet_forge' + +class R10K::MockModule + + # @!attribute [r] title + # @return [String] The forward slash separated owner and name of the module + attr_reader :title + + # @!attribute [r] name + # @return [String] The name of the module + attr_reader :name + + # @param [r] dirname + # @return [String] The name of the directory containing this module + attr_reader :dirname + + # @!attribute [r] owner + # @return [String, nil] The owner of the module if one is specified + attr_reader :owner + + # @!attribute [r] path + # @return [Pathname] The full path of the module + attr_reader :path + + # @!attribute [r] version + # @return [Pathname] The version, if it can be statically determined from args + attr_reader :version + + # @param title [String] + # @param dirname [String] + # @param args [Array] + def initialize(title, dirname, args, environment=nil) + @title = PuppetForge::V3.normalize_name(title) + @dirname = dirname + @owner, @name = parse_title(@title) + @path = Pathname.new(File.join(@dirname, @name)) + @version = find_version(args) + end + + private + + def find_version(args) + if args.is_a?(String) + args + elsif args.is_a?(Hash) + if args[:type] == 'forge' + args[:version] + elsif args[:ref] && args[:ref].match(/[0-9a-f]{40}/) + args[:ref] + elsif args[:type] == 'git' && args[:version].match(/[0-9a-f]{40}/) + args[:version] + else + args[:tag] || args[:commit] + end + end + end + + def parse_title(title) + if (match = title.match(/\A(\w+)\Z/)) + [nil, match[1]] + elsif (match = title.match(/\A(\w+)[-\/](\w+)\Z/)) + [match[1], match[2]] + end + end +end diff --git a/lib/r10k/puppetfile.rb b/lib/r10k/puppetfile.rb index c95c31982..28b17e8eb 100644 --- a/lib/r10k/puppetfile.rb +++ b/lib/r10k/puppetfile.rb @@ -34,6 +34,11 @@ class Puppetfile # @return [String] The path to the Puppetfile attr_reader :puppetfile_path + # @!attrbute [rw] previous_version + # @return [R10K::StubPuppetfile] A simplified version of this Puppetfile + # available on disk prior to the environment sync + attr_accessor :previous_version + # @!attribute [rw] environment # @return [R10K::Environment] Optional R10K::Environment that this Puppetfile belongs to. attr_accessor :environment @@ -130,6 +135,19 @@ def add_module(name, args) args[:default_branch_override] = @default_branch_override end + no_change = false + if @previous_version + stub_module = R10K::MockModule.new(name, install_path, args) + logger.debug _("Checking previous Puppetfile for version %{version} of %{name}" % { version: stub_module.version, name: stub_module.name }) + if stub_module.version == @previous_version.modules[stub_module.name].version + logger.debug _("Expected version has not changed between this and previous deployment, skipping %{name}" % { name: stub_module.name }) + # The version in this Puppetfile is the same as the version previous + # specified in the Puppetfile (or we cannot be sure the versions + # specified are static, eg a branch specifier). + no_change = true + end + end + mod = R10K::Module.new(name, install_path, args, @environment) mod.origin = :puppetfile @@ -144,7 +162,9 @@ def add_module(name, args) @managed_content[install_path] = Array.new unless @managed_content.has_key?(install_path) @managed_content[install_path] << mod.name - @modules << mod + # We want to manage the content (and not purge it) even if we do not want + # to sync modules that we assume unchanged. + no_change ? @modules : @modules << mod end include R10K::Util::Purgeable diff --git a/lib/r10k/stub_puppetfile.rb b/lib/r10k/stub_puppetfile.rb new file mode 100644 index 000000000..f48e28bdd --- /dev/null +++ b/lib/r10k/stub_puppetfile.rb @@ -0,0 +1,191 @@ +require 'thread' +require 'pathname' +require 'r10k/module' +require 'r10k/util/purgeable' +require 'r10k/errors' +require 'r10k/mock_module' + +module R10K +class StubPuppetfile + # Defines the data members of a Puppetfile + + include R10K::Settings::Mixin + + def_setting_attr :pool_size, 4 + + include R10K::Logging + + # @!attribute [r] forge + # @return [String] The URL to use for the Puppet Forge + attr_reader :forge + + # @!attribute [r] modules + # @return [Hash] + attr_reader :modules + + # @!attribute [r] basedir + # @return [String] The base directory that contains the Puppetfile + attr_reader :basedir + + # @!attribute [r] moduledir + # @return [String] The directory to install the modules #{basedir}/modules + attr_reader :moduledir + + # @!attrbute [r] puppetfile_path + # @return [String] The path to the Puppetfile + attr_reader :puppetfile_path + + # @!attribute [rw] environment + # @return [R10K::Environment] Optional R10K::Environment that this Puppetfile belongs to. + attr_accessor :environment + + # @!attribute [rw] force + # @return [Boolean] Overwrite any locally made changes + attr_accessor :force + + # @param [String] basedir + # @param [String] moduledir The directory to install the modules, default to #{basedir}/modules + # @param [String] puppetfile_path The path to the Puppetfile, default to #{basedir}/Puppetfile + # @param [String] puppetfile_name The name of the Puppetfile, default to 'Puppetfile' + # @param [Boolean] force Shall we overwrite locally made changes? + def initialize(basedir, moduledir = nil, puppetfile_path = nil, puppetfile_name = nil, force = nil ) + @basedir = basedir + @force = force || false + @moduledir = moduledir || File.join(basedir, 'modules') + @puppetfile_name = puppetfile_name || 'Puppetfile' + @puppetfile_path = puppetfile_path || File.join(basedir, @puppetfile_name) + + logger.info _("Using Puppetfile '%{puppetfile}'") % {puppetfile: @puppetfile_path} + + @modules = {} + @managed_content = {} + @forge = 'forgeapi.puppetlabs.com' + + @loaded = false + end + + def load(default_branch_override = nil) + return true if self.loaded? + if File.readable? @puppetfile_path + self.load!(default_branch_override) + else + logger.debug _("Puppetfile %{path} missing or unreadable") % {path: @puppetfile_path.inspect} + end + end + + def load!(default_branch_override = nil) + @default_branch_override = default_branch_override + + dsl = R10K::Puppetfile::DSL.new(self) + dsl.instance_eval(puppetfile_contents, @puppetfile_path) + + @loaded = true + rescue SyntaxError, LoadError, ArgumentError, NameError => e + # This type of Puppetfile represents an older version of the currently + # deploying Puppetfile for comparisons sake, we never want an issue with + # the old version to prevent deploying a new Puppetfile. So we swallow + # errors here and deal with potential garbage in our @modules variable later. + end + + def loaded? + @loaded + end + + # @param [String] forge + def set_forge(forge) + @forge = forge + end + + # @param [String] moduledir + def set_moduledir(moduledir) + @moduledir = if Pathname.new(moduledir).absolute? + moduledir + else + File.join(basedir, moduledir) + end + end + + # @param [String] name + # @param [*Object] args + def add_module(name, args) + if args.is_a?(Hash) && install_path = args.delete(:install_path) + install_path = resolve_install_path(install_path) + validate_install_path(install_path, name) + else + install_path = @moduledir + end + + if args.is_a?(Hash) && @default_branch_override != nil + args[:default_branch_override] = @default_branch_override + end + + mod = R10K::MockModule.new(name, install_path, args, @environment) + + # Do not load modules if they would conflict with the attached + # environment + if environment && environment.module_conflicts?(mod) + mod = nil + return @modules + end + + @modules[mod.name] = mod + end + + private + + def puppetfile_contents + File.read(@puppetfile_path) + end + + def resolve_install_path(path) + pn = Pathname.new(path) + + unless pn.absolute? + pn = Pathname.new(File.join(basedir, path)) + end + + # .cleanpath is as good as we can do without touching the filesystem. + # The .realpath methods will also choke if some of the intermediate + # paths are missing, even though we will create them later as needed. + pn.cleanpath.to_s + end + + def validate_install_path(path, modname) + unless /^#{Regexp.escape(real_basedir)}.*/ =~ path + raise R10K::Error.new("Puppetfile cannot manage content '#{modname}' outside of containing environment: #{path} is not within #{real_basedir}") + end + + true + end + + def real_basedir + Pathname.new(basedir).cleanpath.to_s + end + + class DSL + # A barebones implementation of the Puppetfile DSL + # + # @api private + + def initialize(librarian) + @librarian = librarian + end + + def mod(name, args = nil) + @librarian.add_module(name, args) + end + + def forge(location) + @librarian.set_forge(location) + end + + def moduledir(location) + @librarian.set_moduledir(location) + end + + def method_missing(method, *args) + raise NoMethodError, _("unrecognized declaration '%{method}'") % {method: method} + end + end +end +end From c6a3029fd3fe38ef459fc0e60c822e56bb0eabc7 Mon Sep 17 00:00:00 2001 From: Justin Stoller Date: Tue, 13 Apr 2021 14:24:44 -0700 Subject: [PATCH 2/3] (CODEMGMT-1415) Moving logging setup to environment base class --- lib/r10k/environment/base.rb | 3 +++ lib/r10k/environment/git.rb | 3 --- lib/r10k/environment/svn.rb | 2 -- lib/r10k/environment/with_modules.rb | 3 --- spec/unit/environment/base_spec.rb | 1 + 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/r10k/environment/base.rb b/lib/r10k/environment/base.rb index eb4193a95..6da85f4fa 100644 --- a/lib/r10k/environment/base.rb +++ b/lib/r10k/environment/base.rb @@ -1,3 +1,4 @@ +require 'r10k/logging' require 'r10k/util/subprocess' require 'r10k/stub_puppetfile' @@ -6,6 +7,8 @@ # @since 1.3.0 class R10K::Environment::Base + include R10K::Logging + # @!attribute [r] name # @return [String] A name for this environment that is unique to the given source attr_reader :name diff --git a/lib/r10k/environment/git.rb b/lib/r10k/environment/git.rb index bdf90dc84..d4753eaea 100644 --- a/lib/r10k/environment/git.rb +++ b/lib/r10k/environment/git.rb @@ -1,4 +1,3 @@ -require 'r10k/logging' require 'r10k/puppetfile' require 'r10k/git/stateful_repository' require 'forwardable' @@ -8,8 +7,6 @@ # @since 1.3.0 class R10K::Environment::Git < R10K::Environment::WithModules - include R10K::Logging - R10K::Environment.register(:git, self) # Register git as the default environment type R10K::Environment.register(nil, self) diff --git a/lib/r10k/environment/svn.rb b/lib/r10k/environment/svn.rb index f04119fb8..6a244e513 100644 --- a/lib/r10k/environment/svn.rb +++ b/lib/r10k/environment/svn.rb @@ -7,8 +7,6 @@ # @since 1.3.0 class R10K::Environment::SVN < R10K::Environment::Base - include R10K::Logging - R10K::Environment.register(:svn, self) # @!attribute [r] remote diff --git a/lib/r10k/environment/with_modules.rb b/lib/r10k/environment/with_modules.rb index 5f67fb013..d62f73c72 100644 --- a/lib/r10k/environment/with_modules.rb +++ b/lib/r10k/environment/with_modules.rb @@ -1,4 +1,3 @@ -require 'r10k/logging' require 'r10k/util/purgeable' # This abstract base class implements an environment that can include module @@ -7,8 +6,6 @@ # @since 3.4.0 class R10K::Environment::WithModules < R10K::Environment::Base - include R10K::Logging - # @!attribute [r] moduledir # @return [String] The directory to install environment-defined modules # into (default: #{basedir}/modules) diff --git a/spec/unit/environment/base_spec.rb b/spec/unit/environment/base_spec.rb index 7916c3321..afdd51227 100644 --- a/spec/unit/environment/base_spec.rb +++ b/spec/unit/environment/base_spec.rb @@ -53,6 +53,7 @@ before(:each) do allow(mock_puppetfile).to receive(:managed_directories).and_return([]) allow(mock_puppetfile).to receive(:desired_contents).and_return([]) + allow(mock_puppetfile).to receive(:previous_version=) allow(R10K::Puppetfile).to receive(:new).and_return(mock_puppetfile) end From d007728a0584418c41b05ae1f339f76f5345d797 Mon Sep 17 00:00:00 2001 From: Justin Stoller Date: Tue, 13 Apr 2021 15:14:20 -0700 Subject: [PATCH 3/3] (CODEMGMT-1415) gate assumption that modules have not changed on --assume-unchanged flag --- lib/r10k/action/deploy/environment.rb | 3 +++ lib/r10k/cli/deploy.rb | 1 + lib/r10k/environment/base.rb | 16 ++++++++++------ lib/r10k/puppetfile.rb | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/r10k/action/deploy/environment.rb b/lib/r10k/action/deploy/environment.rb index bbe3baeeb..2380e71f5 100644 --- a/lib/r10k/action/deploy/environment.rb +++ b/lib/r10k/action/deploy/environment.rb @@ -89,6 +89,8 @@ def visit_environment(environment) started_at = Time.new @environment_ok = true + environment.read_previous_puppetfile if @assume_unchanged + status = environment.status logger.info _("Deploying environment %{env_path}") % {env_path: environment.path} @@ -186,6 +188,7 @@ def allowed_initialize_opts super.merge(puppetfile: :self, cachedir: :self, 'no-force': :self, + 'assume-unchanged': :self, 'generate-types': :self, 'puppet-path': :self, 'puppet-conf': :self, diff --git a/lib/r10k/cli/deploy.rb b/lib/r10k/cli/deploy.rb index d5ff98bd4..bf6145d3f 100644 --- a/lib/r10k/cli/deploy.rb +++ b/lib/r10k/cli/deploy.rb @@ -64,6 +64,7 @@ def self.command DESCRIPTION flag :p, :puppetfile, 'Deploy modules from a puppetfile' + flag nil, :'assume-unchanged', 'Assume previously deployed modules have not changed' option nil, :'default-branch-override', 'Specify a branchname to override the default branch in the puppetfile', argument: :required diff --git a/lib/r10k/environment/base.rb b/lib/r10k/environment/base.rb index 6da85f4fa..02d467020 100644 --- a/lib/r10k/environment/base.rb +++ b/lib/r10k/environment/base.rb @@ -52,17 +52,21 @@ def initialize(name, basedir, dirname, options = {}) @full_path = File.join(@basedir, @dirname) @path = Pathname.new(File.join(@basedir, @dirname)) + + @puppetfile = R10K::Puppetfile.new(@full_path, nil, nil, @puppetfile_name) + @puppetfile.environment = self + end + + def read_previous_puppetfile + previous_puppetfile = nil if File.exist?(File.join(@full_path, @puppetfile_name || 'Puppetfile')) - @previous_puppetfile = R10K::StubPuppetfile.new(@full_path, nil, nil, @puppetfile_name) + previous_puppetfile = R10K::StubPuppetfile.new(@full_path, nil, nil, @puppetfile_name) else logger.debug _("Could not find Puppetfile at: %{path}" % { path: File.join(@full_path, @puppetfile_name || 'Puppetfile') }) end - @previous_puppetfile.load if @previous_puppetfile - - @puppetfile = R10K::Puppetfile.new(@full_path, nil, nil, @puppetfile_name) - @puppetfile.environment = self - @puppetfile.previous_version = @previous_puppetfile + previous_puppetfile.load if previous_puppetfile + @puppetfile.previous_version = previous_puppetfile end # Synchronize the given environment. diff --git a/lib/r10k/puppetfile.rb b/lib/r10k/puppetfile.rb index 28b17e8eb..17b65b07c 100644 --- a/lib/r10k/puppetfile.rb +++ b/lib/r10k/puppetfile.rb @@ -138,7 +138,7 @@ def add_module(name, args) no_change = false if @previous_version stub_module = R10K::MockModule.new(name, install_path, args) - logger.debug _("Checking previous Puppetfile for version %{version} of %{name}" % { version: stub_module.version, name: stub_module.name }) + logger.debug2 _("Checking previous Puppetfile for version %{version} of %{name}" % { version: stub_module.version, name: stub_module.name }) if stub_module.version == @previous_version.modules[stub_module.name].version logger.debug _("Expected version has not changed between this and previous deployment, skipping %{name}" % { name: stub_module.name }) # The version in this Puppetfile is the same as the version previous