Skip to content

Commit

Permalink
refactor template/context/hash mess into template class, fix a number…
Browse files Browse the repository at this point in the history
… of issues with context, disallow duplicate template names for now, add initial state tracking
  • Loading branch information
wr0ngway committed Sep 7, 2018
1 parent 43584d3 commit e88ed23
Show file tree
Hide file tree
Showing 9 changed files with 993 additions and 645 deletions.
67 changes: 58 additions & 9 deletions lib/simplygenius/atmos/commands/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def self.description
Installs configuration templates used by atmos to create infrastructure
resources e.g.
atmos generate aws-vpc
atmos generate aws/vpc
use --list to get a list of the template names for a given sourceroot
EOF
Expand All @@ -36,45 +36,56 @@ def self.description
option ["-p", "--sourcepath"],
"PATH", "find templates at given path or github url\n",
multivalued: true
option ["-c", "--context"],
"CONTEXT", "provide context variables (dot notation)\n",
multivalued: true

parameter "TEMPLATE ...", "atmos template(s)", required: false

def execute
signal_usage_error "template name is required" if template_list.blank? && ! list?

sourcepaths = []
sourcepath_list.each do |sp|
sourcepaths << SourcePath.new(File.basename(sp), sp)
SourcePath.register(File.basename(sp), sp)
end

# don't want to fail for new repo
if Atmos.config && Atmos.config.is_atmos_repo?
Atmos.config['template_sources'].try(:each) do |item|
sourcepaths << SourcePath.new(item.name, item.location)
SourcePath.register(item.name, item.location)
end
end

# Always search for templates against the bundled templates directory
sourcepaths << SourcePath.new('bundled', File.expand_path('../../../../../templates', __FILE__))
SourcePath.register('bundled', File.expand_path('../../../../../templates', __FILE__))

if list?
logger.info "Valid templates are:"
sourcepaths.each do |sp|
SourcePath.registry.each do |sp|
logger.info("\tSourcepath #{sp}")
filtered_names = sp.template_names.select do |name|
template_list.blank? || template_list.any? {|f| name =~ /#{f}/ }
end
filtered_names.each {|n| logger.info ("\t\t#{n}")}
end
else
g = Generator.new(*sourcepaths,
force: force?,
g = Generator.new(force: force?,
pretend: dryrun?,
quiet: quiet?,
skip: skip?,
dependencies: dependencies?)

begin
g.generate(template_list)

context = SettingsHash.new
context_list.each do |c|
key, value = c.split('=', 2)
context.notation_put(key, value)
end

g.generate(*template_list, context: context)
save_state(g.visited_templates, template_list)

rescue ArgumentError => e
logger.error(e.message)
exit 1
Expand All @@ -83,6 +94,44 @@ def execute

end

def state_file
@state_file ||= Atmos.config["generate.state_file"]
end

def state
@state ||= begin
if state_file.present?
path = File.expand_path(state_file)
yml_hash = {}
if File.exist?(path)
yml_hash = YAML.load_file(path)
end
SettingsHash.new(yml_hash)
else
SettingsHash.new
end
end
end

def save_state(visited_templates, entrypoint_template_names)
if state_file.present?
visited_state = []
visited_templates.each do |tmpl|
visited_tmpl = tmpl.to_h
visited_tmpl[:entrypoint] = true if entrypoint_template_names.include?(tmpl.name)
visited_tmpl[:context] = tmpl.scoped_context.to_h

visited_state << visited_tmpl
end

state[:visited_templates] ||= []
state[:visited_templates].concat(visited_state)
state[:visited_templates].sort! {|h1, h2| h1[:name] <=> h2[:name] }.uniq!

File.write(state_file, YAML.dump(state))
end
end

end

end
Expand Down
163 changes: 57 additions & 106 deletions lib/simplygenius/atmos/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ module Atmos
class Generator

include GemLogger::LoggerSupport
include UI

def initialize(*sourcepaths, **opts)
@sourcepaths = sourcepaths
attr_reader :visited_templates

def initialize(**opts)
if opts.has_key?(:dependencies)
@dependencies = opts.delete(:dependencies)
else
Expand All @@ -22,124 +22,60 @@ def initialize(*sourcepaths, **opts)
@thor_opts = opts
@thor_generators = {}
@resolved_templates = {}
@visited_templates = []
end

# TODO: store/track installed templates in a file in target repo

def generate(template_names, context=SettingsHash.new)
def generate(*template_names, context: {})
seen = Set.new
context = SettingsHash.new(context) unless context.kind_of?(SettingsHash)

Array(template_names).each do |template_name|

tmpl = SettingsHash.new.merge(context).merge({template: template_name})
template_names.each do |template_name|

walk_dependencies(tmpl).each do |tmpl|
tname = tmpl[:template]
seen_tmpl = (tmpl.notation_get(context_path(tname)) || SettingsHash.new).merge({template: tname})
apply_template(tmpl) unless seen.include?(seen_tmpl)
seen << seen_tmpl
end
# clone since we are mutating context and can be called from within a
# template, walk_deps also clones
tmpl = SourcePath.find_template(template_name)
tmpl.clone.context.merge!(context)

end
end

def context_path(name)
name.gsub('-', '_').gsub('/', '.')
end

protected

# TODO: allow fully qualifying dependent templates by source name, e.g. atmos-recipes:scaffold

def sourcepath_for(name)
@resolved_templates[name] ||= begin
sps = @sourcepaths.select do |sp|
sp.template_names.include?(name)
end

sp = nil
if sps.size == 0
raise ArgumentError.new("Could not find template: #{name}")
elsif sps.size > 1
if @thor_opts[:force]
sp = sps.first
else
sp_names = sps.collect(&:name)
sp_names.collect {|n| sp_names.count(n)}

choice = choose do |menu|
menu.prompt = "Which source for template '#{name}'? "
sp_names.each { |n| menu.choice(n) }
menu.default = sp_names.first
end
sp = sps[sp_names.index(choice)]
end
logger.info "Using source '#{sp.name}' for template '#{name}'"
if @dependencies
deps = tmpl.walk_dependencies.to_a
else
sp = sps.first
deps = [tmpl]
end

sp
end
end

# depth first iteration of dependencies
def walk_dependencies(tmpl, seen=Set.new)
Enumerator.new do |yielder|
tmpl = SettingsHash.new(tmpl) unless tmpl.kind_of?(SettingsHash)
name = tmpl[:template]

if @dependencies
if seen.include?(name)
seen << name
raise ArgumentError.new("Circular template dependency: #{seen.to_a.join(" => ")}")
end
seen << name

sp = sourcepath_for(name)
template_dependencies = sp.template_dependencies(name)

template_dependencies.each do |dep|
child = dep.clone
child_name = child.delete(:template)
child = child.merge(tmpl).merge(template: child_name)
walk_dependencies(child, seen.dup).each {|d| yielder << d }
deps.each do |dep_tmpl|
seen_tmpl = [dep_tmpl.name, dep_tmpl.scoped_context]
unless seen.include?(seen_tmpl)
apply_template(dep_tmpl)
visited_templates << dep_tmpl
end
seen << seen_tmpl
end

yielder << tmpl
end

return visited_templates
end

protected

def apply_template(tmpl)
tmpl = SettingsHash.new(tmpl) unless tmpl.kind_of?(SettingsHash)
name = tmpl[:template]
context = tmpl
sp = sourcepath_for(name)

@thor_generators[sp] ||= begin
Class.new(ThorGenerator) do
source_root sp.directory
end
@thor_generators[tmpl.source] ||= Class.new(ThorGenerator) do
source_root tmpl.source.directory
end

gen = @thor_generators[sp].new(name, context, sp, self, **@thor_opts)
gen = @thor_generators[tmpl.source].new(tmpl, self, **@thor_opts)
gen.apply

gen # makes testing easier by giving a handle to thor generator instance
end

class ThorGenerator < Thor

include Thor::Actions
attr_reader :name, :context, :context_path, :source_path, :parent
attr_reader :tmpl, :parent

def initialize(name, context, source_path, parent, **opts)
@name = name
@context = context
@source_path = source_path
def initialize(tmpl, parent, **opts)
@tmpl = tmpl
@parent = parent
@context_path = parent.context_path(name)
super([], **opts)
end

Expand All @@ -149,24 +85,24 @@ def initialize(name, context, source_path, parent, **opts)
include UI

def apply
template_dir = @source_path.template_dir(name)
path = @source_path.directory
template_dir = tmpl.directory
path = tmpl.source.directory

logger.debug("Applying template '#{name}' from '#{template_dir}' in sourcepath '#{path}'")
logger.debug("Applying template '#{tmpl.name}' from '#{template_dir}' in sourcepath '#{path}'")

Find.find(template_dir) do |f|
next if f == template_dir # don't create a directory for the template dir itself, but don't prune so we recurse
Find.prune if f == @source_path.template_config_path(name) # don't copy over templates.yml
Find.prune if f == @source_path.template_actions_path(name) # don't copy over templates.rb
Find.prune if f == tmpl.config_path # don't copy over templates.yml
Find.prune if f == tmpl.actions_path # don't copy over templates.rb

# Using File.join(x, '') to ensure trailing slash to make sure we end
# up with a relative path
template_rel = f.gsub(/#{File.join(template_dir, '')}/, '')
source_rel = f.gsub(/#{File.join(path, '')}/, '')
dest_rel = source_rel.gsub(/^#{File.join(name, '')}/, '')
dest_rel = source_rel.gsub(/^#{File.join(tmpl.name, '')}/, '')

# Only include optional files when their conditions eval to true
optional = @source_path.template_optional(name)[template_rel]
optional = tmpl.optional[template_rel]
if optional
exclude = ! eval(optional)
logger.debug("Optional template '#{template_rel}' with condition: '#{optional}', excluding=#{exclude}")
Expand All @@ -181,11 +117,23 @@ def apply
end
end

eval @source_path.template_actions(name), binding, @source_path.template_actions_path(name)
eval tmpl.actions, binding, tmpl.actions_path
end

def context
tmpl.context
end

def scoped_context
tmpl.scoped_context
end

def lookup_context(varname)
varname.blank? ? nil: context.notation_get("#{context_path}.#{varname}")
varname.blank? ? nil: tmpl.scoped_context[varname]
end

def track_context(varname, value)
varname.blank? || value.nil? ? nil: tmpl.scoped_context[varname] = value
end
end

Expand All @@ -195,6 +143,7 @@ def ask(question, answer_type = nil, varname: nil, &details)
if result.nil?
result = super(question, answer_type, &details)
end
track_context(varname, result)
result
end

Expand All @@ -204,12 +153,14 @@ def agree(question, character = nil, varname: nil, &details)
if result.nil?
result = super(question, character, &details)
end
!!result
result = !!result
track_context(varname, result)
result
end

desc "generate <tmpl_name> [context_hash]", "Generates the given template with optional context"
def generate(name, ctx: context)
parent.send(:generate, [name], ctx)
parent.generate(name, context: ctx.clone)
end

desc "raw_config <yml_filename>", "Loads yml file"
Expand Down Expand Up @@ -247,7 +198,7 @@ def config_present?(yml_file, key, value=nil)
return result
end

desc "new_keys? <src_yml_filename> <dest_yml_filename>", "Tests if src/dest yml have differing top level keys"
desc "new_keys? <src_yml_filename> <dest_yml_filename>", "Tests if src yml has top level keys not present in dest yml"
def new_keys?(src_yml_file, dest_yml_file)
src = raw_config(src_yml_file).keys.sort
dest = raw_config(dest_yml_file).keys.sort
Expand Down

0 comments on commit e88ed23

Please sign in to comment.