Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PDK-1113) Use PDK configuration instead of AnswerFile class #842

Merged
merged 3 commits into from
Feb 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions lib/pdk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,22 @@ module Test
autoload :Unit, 'pdk/tests/unit'
end

# Singleton accessor to the current answer file being used by the PDK.
#
# @return [PDK::AnswerFile] The AnswerFile instance currently being used by
# the PDK.
def self.answers
@answer_file ||= PDK::AnswerFile.new
end

# Specify the path to a custom answer file that the PDK should use.
#
# @param path [String] A path on disk to the file where the PDK should store
# answers to interactive questions.
def self.answer_file=(path)
@answer_file = PDK::AnswerFile.new(path)
end

def self.logger
@logger ||= PDK::Logger.new
end

def self.config
@config ||= PDK::Config.new
return @config unless @config.nil?
options = {}
options['user.module_defaults.path'] = PDK::Util::Env['PDK_ANSWER_FILE'] unless PDK::Util::Env['PDK_ANSWER_FILE'].nil?
@config = PDK::Config.new(options)
end

def self.analytics
@analytics ||= PDK::Analytics.build_client(
logger: PDK.logger,
disabled: PDK::Util::Env['PDK_DISABLE_ANALYTICS'] || PDK.config.user['analytics']['disabled'],
user_id: PDK.config.user['analytics']['user-id'],
disabled: PDK::Util::Env['PDK_DISABLE_ANALYTICS'] || PDK.config.get_within_scopes('analytics.disabled', %w[user system]),
user_id: PDK.config.get_within_scopes('analytics.user-id', %w[user system]),
app_id: "UA-139917834-#{PDK::Util.development_mode? ? '2' : '1'}",
client: :google_analytics,
app_name: 'pdk',
Expand Down
95 changes: 2 additions & 93 deletions lib/pdk/answer_file.rb
Original file line number Diff line number Diff line change
@@ -1,103 +1,12 @@
require 'pdk'
autoload :JSON, 'json'

module PDK
class AnswerFile
attr_reader :answers
attr_reader :answer_file_path

# Initialises the AnswerFile object, which stores the responses to certain
# interactive questions.
#
# @param answer_file_path [String, nil] The path on disk to the file where
# the answers will be stored and read from. If not specified (or `nil`),
# the default path will be used (see #default_answer_file_path).
#
# @raise (see #read_from_disk)
def initialize(answer_file_path = nil)
@answer_file_path = answer_file_path || default_answer_file_path
@answers = read_from_disk
end

# Retrieve the stored answer to a question.
#
# @param question [String] The question name/identifying string.
#
# @return [Object] The answer to the question, or `nil` if no answer found.
def [](question)
answers[question]
end

# Update the stored answers in memory and then save them to disk.
#
# @param new_answers [Hash{String => Object}] The new questions and answers
# to be merged into the existing answers.
#
# @raise [PDK::CLI::FatalError] if the new answers are not provided as
# a Hash.
# @raise (see #save_to_disk)
def update!(new_answers = {})
unless new_answers.is_a?(Hash)
raise PDK::CLI::FatalError, _('Answer file can be updated only with a Hash')
end

answers.merge!(new_answers)

save_to_disk
end

private

# Determine the default path to the answer file.
#
# @return [String] The path on disk to the default answer file.
def default_answer_file_path
File.join(PDK::Util.cachedir, 'answers.json')
end

# Read existing answers into memory from the answer file on disk.
#
# @raise [PDK::CLI::FatalError] If the answer file exists but can not be
# read.
#
# @return [Hash{String => Object}] The existing questions and answers.
def read_from_disk
return {} if !PDK::Util::Filesystem.file?(answer_file_path) || PDK::Util::Filesystem.zero?(answer_file_path)

unless PDK::Util::Filesystem.readable?(answer_file_path)
raise PDK::CLI::FatalError, _("Unable to open '%{file}' for reading") % {
file: answer_file_path,
}
end

answers = JSON.parse(PDK::Util::Filesystem.read_file(answer_file_path))
if answers.is_a?(Hash)
answers
else
PDK.logger.warn _("Answer file '%{path}' did not contain a valid set of answers, recreating it") % {
path: answer_file_path,
}
{}
end
rescue JSON::JSONError
PDK.logger.warn _("Answer file '%{path}' did not contain valid JSON, recreating it") % {
path: answer_file_path,
}
{}
end

# Save the in memory answer set to the answer file on disk.
#
# @raise [PDK::CLI::FatalError] if the answer file can not be written to.
def save_to_disk
PDK::Util::Filesystem.mkdir_p(File.dirname(answer_file_path))

PDK::Util::Filesystem.write_file(answer_file_path, JSON.pretty_generate(answers))
rescue SystemCallError, IOError => e
raise PDK::CLI::FatalError, _("Unable to write '%{file}': %{msg}") % {
file: answer_file_path,
msg: e.message,
}
def self.default_answer_file_path
PDK::Util::Filesystem.expand_path(File.join(PDK::Util.cachedir, 'answers.json'))
end
end
end
5 changes: 0 additions & 5 deletions lib/pdk/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,6 @@ def self.puppet_dev_option(dsl)
flag :d, :debug, _('Enable debug output.') do |_, _|
PDK.logger.enable_debug_output
end

option nil, 'answer-file', _('Path to an answer file.'), argument: :required, hidden: true do |value|
require 'pdk/answer_file'
PDK.answer_file = value
end
end

require 'pdk/cli/bundle'
Expand Down
2 changes: 1 addition & 1 deletion lib/pdk/cli/convert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module PDK::CLI
raise PDK::CLI::ExitWithError, _('You can not specify --template-url and --default-template.') if opts[:'template-url']

opts[:'template-url'] = PDK::Util::TemplateURI.default_template_addressable_uri.to_s
PDK.answers.update!('template-url' => nil)
PDK.config.user['module_defaults']['template-url'] = nil
end

PDK::CLI::Util.validate_template_opts(opts)
Expand Down
40 changes: 35 additions & 5 deletions lib/pdk/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ class Config
autoload :Validator, 'pdk/config/validator'
autoload :YAML, 'pdk/config/yaml'

# Create a new instance of the PDK Configuration
# @param options [Hash[String => String]] Optional hash to override configuration options
# @option options [String] 'system.path' Path to the system PDK configuration file
# @option options [String] 'system.module_defaults.path' Path to the system module answers PDK configuration file
# @option options [String] 'user.path' Path to the user PDK configuration file
# @option options [String] 'user.module_defaults.path' Path to the user module answers PDK configuration file
# @option options [String] 'user.analytics.path' Path to the user analytics PDK configuration file
def initialize(options = nil)
options = {} if options.nil?
@config_options = {
'system.path' => PDK::Config.system_config_path,
'system.module_defaults.path' => PDK::Config.system_answers_path,
'user.path' => PDK::Config.user_config_path,
'user.module_defaults.path' => PDK::AnswerFile.default_answer_file_path,
'user.analytics.path' => PDK::Config.analytics_config_path,
}.merge(options)
end

# The user configuration settings.
# @deprecated This method is only provided as a courtesy until the `pdk set config` CLI and associated changes in this class, are completed.
# Any read-only operations should be using `.get` or `.get_within_scopes`
Expand All @@ -25,24 +43,26 @@ def user
# @return [PDK::Config::Namespace]
# @api private
def system_config
@system ||= PDK::Config::JSON.new('system', file: PDK::Config.system_config_path) do
mount :module_defaults, PDK::Config::JSON.new(file: PDK::Config.system_answers_path)
local_options = @config_options
@system ||= PDK::Config::JSON.new('system', file: local_options['system.path']) do
mount :module_defaults, PDK::Config::JSON.new(file: local_options['system.module_defaults.path'])
end
end

# The user level configuration settings.
# @return [PDK::Config::Namespace]
# @api private
def user_config
@user ||= PDK::Config::JSON.new('user', file: PDK::Config.user_config_path) do
mount :module_defaults, PDK::Config::JSON.new(file: PDK.answers.answer_file_path)
local_options = @config_options
@user ||= PDK::Config::JSON.new('user', file: local_options['user.path']) do
mount :module_defaults, PDK::Config::JSON.new(file: local_options['user.module_defaults.path'])

# Due to the json-schema gem having issues with Windows based paths, and only supporting Draft 05 (or less) do
# not use JSON validation yet. Once PDK drops support for EOL rubies, we will be able to use the json_schemer gem
# Which has much more modern support
# Reference - https://github.com/puppetlabs/pdk/pull/777
# Reference - https://tickets.puppetlabs.com/browse/PDK-1526
mount :analytics, PDK::Config::YAML.new(file: PDK::Config.analytics_config_path, persistent_defaults: true) do
mount :analytics, PDK::Config::YAML.new(file: local_options['user.analytics.path'], persistent_defaults: true) do
setting :disabled do
validate PDK::Config::Validator.boolean
default_to { PDK::Config.bolt_analytics_config.fetch('disabled', true) }
Expand Down Expand Up @@ -112,6 +132,16 @@ def get_within_scopes(setting_name, scopes = nil)
nil
end

# Yields a configuration setting value by name, using scope precedence rules. If no scopes are passed, then all scopes are queried using the default precedence rules
# @setting_name [String, Array[String]] The setting name to retrieve without the leading scope name e.g. Use 'setting' instead of 'system.setting'
# @scopes [Nil, Array[String]] The list of scopes, in order, to query in turn for the setting_name. Invalid or missing scopes are ignored.
# @yield [PDK::Config::Namespace, Object] The value of the configuration setting. Does not yield if the setting does not exist or is nil
def with_scoped_value(setting_name, scopes = nil)
raise ArgumentError, _('must be passed a block') unless block_given?
value = get_within_scopes(setting_name, scopes)
yield value unless value.nil?
end

def self.bolt_analytics_config
file = PDK::Util::Filesystem.expand_path('~/.puppetlabs/bolt/analytics.yaml')
PDK::Config::YAML.new(file: file)
Expand Down
13 changes: 11 additions & 2 deletions lib/pdk/config/namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, &
@persistent_defaults = persistent_defaults
@mounts = {}
@loaded_from_file = false
@read_only = false

instance_eval(&block) if block_given?
end
Expand Down Expand Up @@ -206,6 +207,14 @@ def include_in_parent?
child_namespace? && file.nil?
end

# Disables the namespace, and child namespaces, from writing changes to disk.
# Typically this is only needed for unit testing.
# @api private
def read_only!
@read_only = true
@mounts.each { |_, child| child.read_only! }
end

private

# Returns the object class to create settings with. Subclasses may override this to use specific setting classes
Expand Down Expand Up @@ -292,7 +301,7 @@ def load_data(filename)
return if filename.nil?
return unless PDK::Util::Filesystem.file?(filename)

PDK::Util::Filesystem.read_file(file)
PDK::Util::Filesystem.read_file(filename)
rescue Errno::ENOENT => e
raise PDK::Config::LoadError, e.message
rescue Errno::EACCES
Expand All @@ -313,7 +322,7 @@ def load_data(filename)
#
# @return [nil]
def save_data
return if file.nil?
return if file.nil? || @read_only

PDK::Util::Filesystem.mkdir_p(File.dirname(file))

Expand Down
20 changes: 8 additions & 12 deletions lib/pdk/generate/module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ def self.invoke(opts = {})
# If the user specifies our default template url via the command
# line, remove the saved template-url answer so that the template_uri
# resolution can find new default URLs in the future.
PDK.answers.update!('template-url' => nil) if opts.key?(:'template-url')
PDK.config.user['module_defaults']['template-url'] = nil if opts.key?(:'template-url')
else
# Save the template-url answers if the module was generated using a
# template/reference other than ours.
PDK.answers.update!('template-url' => template_uri.metadata_format)
PDK.config.user['module_defaults']['template-url'] = template_uri.metadata_format
end

begin
Expand Down Expand Up @@ -135,13 +135,13 @@ def self.prepare_metadata(opts = {})
require 'pdk/answer_file'
require 'pdk/module/metadata'

opts[:username] = (opts[:username] || PDK.answers['forge_username'] || username_from_login).downcase
opts[:username] = (opts[:username] || PDK.config.get_within_scopes('module_defaults.forge_username') || username_from_login).downcase

defaults = PDK::Module::Metadata::DEFAULTS.dup

defaults['name'] = "#{opts[:username]}-#{opts[:module_name]}" unless opts[:module_name].nil?
defaults['author'] = PDK.answers['author'] unless PDK.answers['author'].nil?
defaults['license'] = PDK.answers['license'] unless PDK.answers['license'].nil?
PDK.config.with_scoped_value('module_defaults.author') { |val| defaults['author'] = val }
PDK.config.with_scoped_value('module_defaults.license') { |val| defaults['license'] = val }
defaults['license'] = opts[:license] if opts.key?(:license)

metadata = PDK::Module::Metadata.new(defaults)
Expand Down Expand Up @@ -342,13 +342,9 @@ def self.module_interview(metadata, opts = {})
end

require 'pdk/answer_file'
PDK.answers.update!(
{
'forge_username' => opts[:username],
'author' => answers['author'],
'license' => answers['license'],
}.delete_if { |_key, value| value.nil? },
)
PDK.config.user['module_defaults']['forge_username'] = opts[:username] unless opts[:username].nil?
PDK.config.user['module_defaults']['author'] = answers['author'] unless answers['author'].nil?
PDK.config.user['module_defaults']['license'] = answers['license'] unless answers['license'].nil?
end
end
end
Expand Down
7 changes: 4 additions & 3 deletions lib/pdk/util/template_uri.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,11 @@ def self.templates(opts)
else
nil
end
answers_uri = if [PACKAGED_TEMPLATE_KEYWORD, DEPRECATED_TEMPLATE_URL].include?(PDK.answers['template-url'])
default_template_url = PDK.config.get_within_scopes('module_defaults.template-url')
answers_uri = if [PACKAGED_TEMPLATE_KEYWORD, DEPRECATED_TEMPLATE_URL].include?(default_template_url)
Addressable::URI.parse(default_template_uri)
elsif PDK.answers['template-url']
new(uri_safe(PDK.answers['template-url'])).uri
elsif default_template_url
new(uri_safe(default_template_url)).uri
else
nil
end
Expand Down
21 changes: 17 additions & 4 deletions spec/acceptance/convert_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,30 @@
context 'when converting to the default template' do
include_context 'in a new module', 'non_default_template'

answer_file = File.join('..', 'non_default_template_answers.json')
around(:each) do |example|
old_answer_file = ENV['PDK_ANSWER_FILE']
ENV['PDK_ANSWER_FILE'] = File.expand_path(File.join('..', 'non_default_template_answers.json'))
example.run
ENV['PDK_ANSWER_FILE'] = old_answer_file
end
glennsarti marked this conversation as resolved.
Show resolved Hide resolved

describe command("pdk convert --default-template --force --answer-file #{answer_file}") do
describe command('pdk convert --default-template --force') do
its(:exit_status) { is_expected.to eq(0) }

# Note - The following file(...) tests can not be run in isolation and require the above
# `its(:exit_status)` to have run first.
describe file('metadata.json') do
its(:content_as_json) { is_expected.to include('template-url' => "#{template_repo}#master") }
end

describe file(answer_file) do
its(:content_as_json) { is_expected.to include('template-url' => nil) }
describe file(File.join('..', 'non_default_template_answers.json')) do
# Due to the serverspec file() resolving on the first pass, the file name will not be correct on the
# second pass. To get around this we use the subject(...) to resolve the filename correctly.
subject(:file_under_test) { file(File.join('..', 'non_default_template_answers.json')) }

it 'has a nil template-url' do
glennsarti marked this conversation as resolved.
Show resolved Hide resolved
expect(file_under_test.content_as_json['template-url']).to be_nil
end
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions spec/acceptance/support/in_a_new_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
argv = [
'pdk', 'new', 'module', name,
'--skip-interview',
'--template-url', template,
'--answer-file', File.join(Dir.pwd, "#{name}_answers.json")
'--template-url', template
]
output, status = Open3.capture2e(*argv)
env = { 'PDK_ANSWER_FILE' => File.join(Dir.pwd, "#{name}_answers.json") }
output, status = Open3.capture2e(env, *argv)

raise "Failed to create test module:\n#{output}" unless status.success?

Expand Down
Loading