diff --git a/lib/bolt/config/options.rb b/lib/bolt/config/options.rb index ed59be3d7..0c39ea822 100644 --- a/lib/bolt/config/options.rb +++ b/lib/bolt/config/options.rb @@ -484,6 +484,14 @@ module Options }, _plugin: true }, + "puppetfile" => { + description: "The path to the project's Puppetfile, used by `bolt module install` and " \ + "related commands. Relative paths are resolved from the project directory; " \ + "absolute paths are used as-is. Defaults to `Puppetfile` in the project directory.", + type: String, + _example: "control-repo/Puppetfile", + _plugin: true + }, "rerunfile" => { description: "The path to the project's rerun file. The rerun file is used to store information " \ "about targets from the most recent run. Expands relative to the project directory.", @@ -668,6 +676,7 @@ module Options policies puppetdb puppetdb-instances + puppetfile rerunfile save-rerun spinner diff --git a/lib/bolt/module_installer/puppetfile.rb b/lib/bolt/module_installer/puppetfile.rb index 24f0a2f47..1c09ff336 100644 --- a/lib/bolt/module_installer/puppetfile.rb +++ b/lib/bolt/module_installer/puppetfile.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'fileutils' + require_relative '../../bolt/error' require_relative 'puppetfile/forge_module' require_relative 'puppetfile/git_module' @@ -69,6 +71,7 @@ def self.parse(path, skip_unsupported_modules: false) # modules. # def write(path, moduledir = nil) + FileUtils.mkdir_p(File.dirname(path)) File.open(path, 'w') do |file| if moduledir file.puts "# This Puppetfile is managed by Bolt. Do not edit." diff --git a/lib/bolt/project.rb b/lib/bolt/project.rb index e7e9b2546..761a9bc5f 100644 --- a/lib/bolt/project.rb +++ b/lib/bolt/project.rb @@ -132,6 +132,15 @@ def initialize(data, path, type = 'option') @rerunfile = File.expand_path(@data['rerunfile'], @path) end + if @data['puppetfile'].is_a?(String) + expanded = File.expand_path(@data['puppetfile'], @path) + unless expanded.start_with?(@path.to_s + '/') + raise Bolt::ValidationError, + "Option 'puppetfile' must be a relative path within the project directory." + end + @puppetfile = Pathname.new(expanded) + end + validate if project_file? end diff --git a/schemas/bolt-project.schema.json b/schemas/bolt-project.schema.json index d65c33b33..b43c95ff2 100644 --- a/schemas/bolt-project.schema.json +++ b/schemas/bolt-project.schema.json @@ -67,6 +67,9 @@ "puppetdb-instances": { "$ref": "#/definitions/puppetdb-instances" }, + "puppetfile": { + "$ref": "#/definitions/puppetfile" + }, "rerunfile": { "$ref": "#/definitions/rerunfile" }, @@ -605,6 +608,17 @@ } ] }, + "puppetfile": { + "description": "The path to the project's Puppetfile, used by `bolt module install` and related commands. Relative paths are resolved from the project directory; absolute paths are used as-is. Defaults to `Puppetfile` in the project directory.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "rerunfile": { "description": "The path to the project's rerun file. The rerun file is used to store information about targets from the most recent run. Expands relative to the project directory.", "oneOf": [ diff --git a/spec/integration/module_installer/module_installer_spec.rb b/spec/integration/module_installer/module_installer_spec.rb index abf1ebbbd..0a040964c 100644 --- a/spec/integration/module_installer/module_installer_spec.rb +++ b/spec/integration/module_installer/module_installer_spec.rb @@ -57,6 +57,22 @@ end end + context 'with a puppetfile override in bolt-project.yaml' do + let(:project_config) { { 'puppetfile' => 'custom/Puppetfile' } } + + it 'writes the puppetfile at the override path and not the default' do + output = run_cli_json(%w[module add puppetlabs-yaml], project: project) + expect(output['puppetfile']).to eq((project.path + 'custom' + 'Puppetfile').to_s) + expect(output['success']).to be + + expect(File.exist?(project.path + 'custom' + 'Puppetfile')).to be true + expect(File.exist?(project.path + 'Puppetfile')).to be false + + puppetfile_content = File.read(project.path + 'custom' + 'Puppetfile') + expect(puppetfile_content.lines).to include(%r{mod 'puppetlabs/yaml'}) + end + end + context 'with install configuration' do let(:base_config) do { diff --git a/spec/unit/project_spec.rb b/spec/unit/project_spec.rb index 19a8b14ce..f263c5c94 100644 --- a/spec/unit/project_spec.rb +++ b/spec/unit/project_spec.rb @@ -81,6 +81,51 @@ end end + describe "#puppetfile" do + context "without a puppetfile override" do + let(:project_config) { {} } + + it "defaults to /Puppetfile" do + expect(project.puppetfile).to be_a(Pathname) + expect(project.puppetfile).to eq(project.path + 'Puppetfile') + end + end + + context "with a relative puppetfile override" do + let(:project_config) { { 'puppetfile' => 'custom/Puppetfile.dev' } } + + it "resolves the path relative to the project directory" do + expect(project.puppetfile).to be_a(Pathname) + expect(project.puppetfile).to eq(project.path + 'custom' + 'Puppetfile.dev') + end + end + + context "with an absolute puppetfile path" do + let(:project_config) { { 'puppetfile' => File.expand_path('/tmp/shared/Puppetfile') } } + + it "raises a validation error" do + expect { project }.to raise_error(Bolt::ValidationError, /must be a relative path within the project directory/) + end + end + + context "with a puppetfile path that escapes the project directory" do + let(:project_config) { { 'puppetfile' => '../outside/Puppetfile' } } + + it "raises a validation error" do + expect { project }.to raise_error(Bolt::ValidationError, /must be a relative path within the project directory/) + end + end + + context "with a non-string puppetfile value" do + let(:project_config) { { 'puppetfile' => 42 } } + + it "fails project schema validation" do + expect { Bolt::Project.create_project(@project_path) } + .to raise_error(Bolt::ValidationError, /puppetfile/) + end + end + end + describe "with namespaced project names" do let(:project_config) { { 'name' => 'puppetlabs-foo' } }