Skip to content

Commit

Permalink
(PDK-1612) Add PDK::Context and context detection
Browse files Browse the repository at this point in the history
Previously the PDK assumed it was always within a Puppet Module, but the PDK can
be used in far more different contexts.  This commit implements the PDK Context
RFC [1], in preparation for other parts of the PDK to consume it, for example
PDK validators:

* Adds the base Context and three concrete contexts: None, Module and
  ControlRepo
* Adds a memoized context object to PDK (PDK.context), for consumption by other
  parts of the PDK
* Updates the module pdk compatibility methods to accept a module path instead
  of assume the PDK is always in a module. This is needed for the Module context
  to check if an arbitrary module is PDK compatible
* Adds two test fixtures (a Puppet module and a control repo) for use by rspec
  for contex detection
* Adds tests for context detection via PDK::Context.create

[1] https://github.com/puppetlabs/pdk-planning/blob/master/RFCs/0007-add-pdk-context.md
  • Loading branch information
glennsarti committed Feb 18, 2020
1 parent d3f2dc7 commit d075099
Show file tree
Hide file tree
Showing 18 changed files with 525 additions and 5 deletions.
5 changes: 5 additions & 0 deletions lib/pdk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module PDK
autoload :AnswerFile, 'pdk/answer_file'
autoload :Bolt, 'pdk/bolt'
autoload :Config, 'pdk/config'
autoload :Context, 'pdk/context'
autoload :ControlRepo, 'pdk/control_repo'
autoload :Generate, 'pdk/generate'
autoload :Logger, 'pdk/logger'
Expand Down Expand Up @@ -54,6 +55,10 @@ def self.config
@config ||= PDK::Config.new
end

def self.context
@context ||= PDK::Context.create(Dir.pwd)
end

def self.analytics
@analytics ||= PDK::Analytics.build_client(
logger: PDK.logger,
Expand Down
97 changes: 97 additions & 0 deletions lib/pdk/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
require 'pdk'

module PDK
module Context
autoload :None, 'pdk/context/none'
autoload :Module, 'pdk/context/module'
autoload :ControlRepo, 'pdk/context/control_repo'

# Automatically determines the PDK Context given a path. Create will continue up the directory tree until it
# finds a valid context
# @return [PDK::Context::AbstractContext] Returns a PDK::Context::None if the context could not be determined
def self.create(context_path)
return PDK::Context::None.new(context_path) unless PDK::Util::Filesystem.directory?(context_path)

previous = nil
current = PDK::Util::Filesystem.expand_path(context_path)
until !PDK::Util::Filesystem.directory?(current) || current == previous
# Control Repo detection
return PDK::Context::ControlRepo.new(current, context_path) if PDK::ControlRepo.control_repo_root?(current)

# Puppet Module detection
# Note - The metadata.json file check should really be in PDK::Util.in_module_root?
metadata_file = File.join(current, 'metadata.json')
if PDK::Util::Filesystem.file?(metadata_file) || PDK::Util.in_module_root?(context_path)
return PDK::Context::Module.new(current, context_path)
end

previous = current
current = PDK::Util::Filesystem.expand_path('..', current)
end
PDK::Context::None.new(context_path)
end

# Abstract class which all PDK Contexts will subclass from.
# @abstract
class AbstractContext
# The root of this context, for example the module root when inside a module. This is different from context_path
# For example a Module context_path could be /path/to/module/manifests/ but the root_path will be /path/to/module as
# that is the root of the Module context
# @return [String, Nil]
attr_reader :root_path

# The path used to create this context, for example the current working directory. This is different from root_path
# For example a Module context_path could be /path/to/module/manifests/ but the root_path will be /path/to/module as
# that is the root of the Module context
# @return [String]
attr_reader :context_path

# @param context_path [String] The path where this context was created from e.g. Dir.pwd
def initialize(context_path)
@context_path = context_path
end

# Whether the current context is compatible with the PDK e.g. in a Module context, whether it has the correct metadata.json content
# @return [Boolean] Default is not compatible
def pdk_compatible?
false
end

# The friendly name to display for this context
# @api private
# @abstract
def display_name; end

# The context which this context is in. For example a Module Context (/controlrepo/site/profile) can be inside of a Control Repo context (/controlrepo)
# The default is to search in the parent directory of this context
# @return [PDK::Context::AbstractContext, Nil] Returns the parent context or nil if there is no parent.
def parent_context
# Default detection is just look for the context in the parent directory of this context
@parent_context || PDK::Context.create(File.dirname(root_path))
end

# Writes the current context information, and parent contexts, to the PDK Debug Logger.
# This is mainly used by the PDK CLI when in debug mode to assist users to figure out why the PDK is misbehaving.
# @api private
def to_debug_log
current = self
depth = 1
loop do
PDK.logger.debug("Detected #{current.display_name} at #{current.root_path.nil? ? current.context_path : current.root_path}")
current = current.parent_context
break if current.nil?
depth += 1
# Circuit breaker in case there are circular references
break if depth > 20
end
nil
end

#:nocov: There's nothing to test here
def to_s
"#<#{self.class}:#{object_id}>#{context_path}"
end
#:nocov:
end
end
end
29 changes: 29 additions & 0 deletions lib/pdk/context/control_repo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'pdk'

module PDK
module Context
# Represents a context for a directory based Control Repository
class ControlRepo < PDK::Context::AbstractContext
# @param repo_root [String] The root path for the control repo.
# @param context_path [String] The path where this context was created from e.g. Dir.pwd
# @see PDK::Context::AbstractContext
def initialize(repo_root, context_path)
super(context_path)
@root_path = repo_root
end

def pdk_compatible?
# Currently there is nothing to determine compatibility with the PDK for a
# Control Repo. For now assume everything is compatible
true
end

#:nocov:
# @see PDK::Context::AbstractContext.display_name
def display_name
_('a Control Repository context')
end
#:nocov:
end
end
end
28 changes: 28 additions & 0 deletions lib/pdk/context/module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'pdk'

module PDK
module Context
# Represents a context for a Puppet Module
class Module < PDK::Context::AbstractContext
# @param module_root [String] The root path for the module.
# @param context_path [String] The path where this context was created from e.g. Dir.pwd
# @see PDK::Context::AbstractContext
def initialize(module_root, context_path)
super(context_path)
@root_path = module_root
end

# @see PDK::Context::AbstractContext.pdk_compatible?
def pdk_compatible?
PDK::Util.module_pdk_compatible?(root_path)
end

#:nocov:
# @see PDK::Context::AbstractContext.display_name
def display_name
_('a Puppet Module context')
end
#:nocov:
end
end
end
22 changes: 22 additions & 0 deletions lib/pdk/context/none.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'pdk'

module PDK
module Context
# Represents a context which the PDK does not know. For example
# an empty directory
class None < PDK::Context::AbstractContext
#:nocov:
# @see PDK::Context::AbstractContext.display_name
def display_name
_('an unknown context')
end
#:nocov:

# @see PDK::Context::AbstractContext.parent_context
def parent_context
# An unknown context has no parent
nil
end
end
end
end
12 changes: 7 additions & 5 deletions lib/pdk/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -248,16 +248,18 @@ def targets_relative_to_pwd(targets)
module_function :targets_relative_to_pwd

# TO-DO: Refactor replacement of lib/pdk/module/build.rb:metadata to use this function instead
def module_metadata
# @param module_path [String] The path to the root of the module. Default is determine the module root automatically
def module_metadata(module_path = nil)
require 'pdk/module/metadata'

PDK::Module::Metadata.from_file(File.join(module_root, 'metadata.json')).data
module_path = module_root if module_path.nil?
PDK::Module::Metadata.from_file(File.join(module_path, 'metadata.json')).data
end
module_function :module_metadata

# TO-DO: Refactor replacement of lib/pdk/module/build.rb:module_pdk_compatible? to use this function instead
def module_pdk_compatible?
['pdk-version', 'template-url'].any? { |key| module_metadata.key?(key) }
# @param module_path [String] The path to the root of the module. Default is determine the module root automatically
def module_pdk_compatible?(module_path = nil)
['pdk-version', 'template-url'].any? { |key| module_metadata(module_path).key?(key) }
end
module_function :module_pdk_compatible?

Expand Down
4 changes: 4 additions & 0 deletions spec/fixtures/control_repo/Puppetfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
forge 'https://forge.puppetlabs.com/'

# Modules from the Puppet Forge
mod 'puppetlabs-stdlib', '1.0.0'
Empty file.
2 changes: 2 additions & 0 deletions spec/fixtures/control_repo/environment.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
modulepath = modules:site:$basemodulepath
environment_timeout = 0
Empty file.
Empty file.
Empty file.
9 changes: 9 additions & 0 deletions spec/fixtures/puppet_module/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "testfixture-valid",
"version": "0.1.0",
"author": "testfixture",
"summary": "Skeleton module test fixture",
"license": "Apache-2.0",
"source": "http://localhost",
"dependencies": []
}
20 changes: 20 additions & 0 deletions spec/unit/pdk/context/control_repo_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'spec_helper'
require 'pdk/context'

describe PDK::Context::ControlRepo do
subject(:context) { described_class.new(repo_root, nil) }

let(:repo_root) { File.join(FIXTURES_DIR, 'control_repo') }

it 'subclasses PDK::Context::AbstractContext' do
expect(context).is_a?(PDK::Context::AbstractContext)
end

it 'remembers the repo root' do
expect(context.root_path).to eq(repo_root)
end

it 'is PDK compatible' do
expect(context.pdk_compatible?).to eq(true)
end
end
23 changes: 23 additions & 0 deletions spec/unit/pdk/context/module_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'spec_helper'
require 'pdk/context'

describe PDK::Context::Module do
subject(:context) { described_class.new(module_root, nil) }

let(:module_root) { File.join(FIXTURES_DIR, 'puppet_module') }

it 'subclasses PDK::Context::AbstractContext' do
expect(context).is_a?(PDK::Context::AbstractContext)
end

it 'remembers the module root' do
expect(context.root_path).to eq(module_root)
end

describe '.pdk_compatible?' do
it 'calls PDK::Util to determine compatibility' do
expect(PDK::Util).to receive(:module_pdk_compatible?).with(context.root_path).and_return(true)
expect(context.pdk_compatible?).to eq(true)
end
end
end
14 changes: 14 additions & 0 deletions spec/unit/pdk/context/none_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require 'spec_helper'
require 'pdk/context'

describe PDK::Context::None do
subject(:context) { described_class.new(nil) }

it 'subclasses PDK::Context::AbstractContext' do
expect(context).is_a?(PDK::Context::AbstractContext)
end

it 'has no parent context' do
expect(context.parent_context).to be_nil
end
end
Loading

0 comments on commit d075099

Please sign in to comment.