Skip to content

Commit

Permalink
Add interactive prompting for missing parameters on create/update. (#161
Browse files Browse the repository at this point in the history
)
  • Loading branch information
askreet committed Oct 23, 2016
1 parent 1e0fce3 commit 274e214
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 28 deletions.
2 changes: 2 additions & 0 deletions lib/moonshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ module Plugins
'stack_events_poller',
'merge_strategy',
'default_strategy',
'ask_user_source',
'always_use_default_source',

# Built-in mechanisms
'artifact_repository/s3_bucket',
Expand Down
17 changes: 17 additions & 0 deletions lib/moonshot/always_use_default_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Moonshot
# The AlwaysUseDefaultSource will always use the previous value in
# the stack, or use the default value during stack creation. This is
# useful if plugins provide the value for a parameter, and we don't
# want to prompt the user for an override. Of course, overrides from
# answer files or command-line arguments will always apply.
class AlwaysUseDefaultSource
def get(sp)
unless sp.default?
raise "Parameter #{sp.name} does not have a default, cannot use AlwaysUseDefaultSource!"
end

# Don't do anything, the default will apply on create, and the
# previous value will be used on update.
end
end
end
38 changes: 38 additions & 0 deletions lib/moonshot/ask_user_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require 'colorize'

module Moonshot
class AskUserSource
def get(sp)
return unless Moonshot.config.interactive

@sp = sp

prompt
loop do
input = gets.chomp

if String(input).empty? && @sp.default?
# We will use the default value, print it here so the output is clear.
puts 'Using default value.'
return
elsif String(input).empty?
puts "Cannot proceed without value for #{@sp.name}!"
else
@sp.set(String(input))
return
end

prompt
end
end

private

def prompt
print "(#{@sp.name})".light_black
print " #{@sp.description}" unless @sp.description.empty?
print " [#{@sp.default}]".light_black if @sp.default?
print ': '
end
end
end
7 changes: 6 additions & 1 deletion lib/moonshot/commands/parameter_arguments.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# rubocop:disable LineLength
module Moonshot
module Commands
module ParameterArguments
def parser
parser = super

parser.on('--[no-]interactive', TrueClass, 'Use interactive prompts for gathering missing configuration.') do |v|
Moonshot.config.interactive = v
end

parser.on('--answer-file FILE', '-aFILE', 'Load Stack Parameters from a YAML file') do |v|
Moonshot.config.answer_file = File.expand_path(v)
end

parser.on('--parameter KEY=VALUE', '-PKEY=VALUE', 'Specify Stack Parameter on the command line') do |v| # rubocop:disable LineLength
parser.on('--parameter KEY=VALUE', '-PKEY=VALUE', 'Specify Stack Parameter on the command line') do |v|
data = v.split('=', 2)
unless data.size == 2
raise "Invalid parameter format '#{v}', expected KEY=VALUE (e.g. MyStackParameter=12)"
Expand Down
33 changes: 26 additions & 7 deletions lib/moonshot/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def list
Moonshot::StackLister.new(@config.app_name).list
end

def create
def create # rubocop:disable AbcSize
# Scan the template for all required parameters and configure
# the ParameterCollection.
@config.parameters = ParameterCollection.from_template(stack.template)
Expand All @@ -42,7 +42,13 @@ def create

# Interview the user for missing parameters, using the
# appropriate prompts.
# TODO See #148
@config.parameters.values.each do |sp|
next if sp.set?

parameter_source = @config.parameter_sources.fetch(sp.name,
@config.default_parameter_source)
parameter_source.get(sp)
end

# Plugins get the final say on parameters before create,
# allowing them to manipulate user supplied input and answers
Expand All @@ -60,17 +66,19 @@ def create
if stack_ok # rubocop:disable GuardClause
run_hook(:deploy, :post_create)
run_plugins(:post_create)
else
raise 'Stack creation failed!'
end
end

def update
def update # rubocop:disable AbcSize
# Scan the template for all required parameters and configure
# the ParameterCollection.
@config.parameters = ParameterCollection.from_template(stack.template)

# Set all values already provided by the stack to UsePreviousValue.
stack.parameters.each do |key, _|
@config.parameters[key].use_previous! if @config.parameters.key?(key)
stack.parameters.each do |key, value|
@config.parameters[key].use_previous!(value) if @config.parameters.key?(key)
end

# Import all Outputs from parent stacks as Parameters on this
Expand All @@ -91,7 +99,13 @@ def update

# Interview the user for missing parameters, using the
# appropriate prompts.
# TODO See #148
@config.parameters.values.each do |sp|
next if sp.set?

parameter_source = @config.parameter_sources.fetch(sp.name,
@config.default_parameter_source)
parameter_source.get(sp)
end

# Plugins get the final say on parameters before create,
# allowing them to manipulate user supplied input and answers
Expand Down Expand Up @@ -139,6 +153,12 @@ def deploy_version(version_name)
end

def delete
# Populate the current values of parameters, for use by plugins.
@config.parameters = ParameterCollection.from_template(stack.template)
stack.parameters.each do |key, value|
@config.parameters[key].use_previous!(value) if @config.parameters.key?(key)
end

run_plugins(:pre_delete)
run_hook(:deploy, :pre_delete)
stack.delete
Expand All @@ -147,7 +167,6 @@ def delete
end

def doctor
# @todo use #run_hook when Stack becomes an InfrastructureProvider
success = true
success &&= stack.doctor_hook
success &&= run_hook(:build, :doctor)
Expand Down
23 changes: 14 additions & 9 deletions lib/moonshot/controller_config.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require_relative 'default_strategy'
require_relative 'ssh_config'
require_relative 'task'
require_relative 'ask_user_source'

module Moonshot
# Holds configuration for Moonshot::Controller
Expand All @@ -18,6 +19,8 @@ class ControllerConfig
attr_accessor :parameter_overrides
attr_accessor :parameters
attr_accessor :parent_stacks
attr_accessor :default_parameter_source
attr_accessor :parameter_sources
attr_accessor :plugins
attr_accessor :project_root
attr_accessor :show_all_stack_events
Expand All @@ -27,15 +30,17 @@ class ControllerConfig
attr_accessor :ssh_instance

def initialize
@interactive = true
@interactive_logger = InteractiveLogger.new
@parameters = ParameterCollection.new
@parameter_overrides = {}
@parent_stacks = []
@plugins = []
@project_root = Dir.pwd
@show_all_stack_events = false
@ssh_config = SSHConfig.new
@default_parameter_source = AskUserSource.new
@interactive = true
@interactive_logger = InteractiveLogger.new
@parameter_overrides = {}
@parameter_sources = {}
@parameters = ParameterCollection.new
@parent_stacks = []
@plugins = []
@project_root = Dir.pwd
@show_all_stack_events = false
@ssh_config = SSHConfig.new

@dev_build_name_proc = lambda do |c|
['dev', c.app_name, c.environment_name, Time.now.to_i].join('/')
Expand Down
5 changes: 5 additions & 0 deletions lib/moonshot/parent_stack_parameter_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ def initialize(config)

def load!
@config.parent_stacks.each do |stack_name|
count = 0

resp = cf_client.describe_stacks(stack_name: stack_name)
raise "Parent Stack #{stack_name} not found!" unless resp.stacks.size == 1

Expand All @@ -14,8 +16,11 @@ def load!
next unless @config.parameters.key?(output.output_key)
# Our Stack has a Parameter matching this output. Set it's
# value to the Output's value.
count += 1
@config.parameters.fetch(output.output_key).set(output.output_value)
end

puts "Imported #{count} parameters from parent stack #{stack_name.blue}!" if count > 0
end
end

Expand Down
18 changes: 9 additions & 9 deletions lib/moonshot/stack_parameter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ module Moonshot
class StackParameter
attr_reader :name
attr_reader :default
attr_reader :description

def initialize(name, default: nil, use_previous: false)
@name = name
@default = default
def initialize(name, default: nil, use_previous: false, description: '')
@default = default
@description = description
@name = name
@use_previous = use_previous
@value = nil
@value = nil
end

# Does this Stack Parameter have a default value that will be used?
Expand All @@ -29,11 +31,13 @@ def set(value)
@use_previous = false
end

def use_previous!
def use_previous!(value)
if @value
raise "Value already set for StackParameter #{@name}, cannot use previous value!"
end

# Make the current value available to plugins.
@value = value
@use_previous = true
end

Expand All @@ -42,10 +46,6 @@ def value
raise "No value set and no default for StackParameter #{@name}!"
end

if @use_previous
raise "StackParameter #{@name} is using previous value, not set!"
end

@value || default
end

Expand Down
4 changes: 3 additions & 1 deletion lib/moonshot/stack_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ def initialize(filename)

def parameters
template_body.fetch('Parameters', {}).map do |k, v|
StackParameter.new(k, default: v['Default'])
StackParameter.new(k,
default: v['Default'],
description: v.fetch('Description', ''))
end
end

Expand Down
17 changes: 16 additions & 1 deletion spec/moonshot/controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
subject { Moonshot::Controller.new }

let(:stack) { instance_double(Moonshot::Stack) }
let(:ask_user_source) { instance_double(Moonshot::AskUserSource) }

let(:cf_client_stubs) do
{
Expand All @@ -28,6 +29,8 @@

expect(stack).to receive(:template)
.and_return(Moonshot::YamlStackTemplate.new(fixture_path('create1.yml')))

subject.config.default_parameter_source = ask_user_source
end

# Scenario:
Expand All @@ -44,7 +47,12 @@
subject.config.parent_stacks = ['parent-stack-1']
subject.config.answer_file = fixture_path('answer1.yml')
subject.config.parameter_overrides['InputParameter4'] = 'Override4'
expect(stack).to receive(:create)

expect(ask_user_source).to receive(:get) do |sp|
expect(sp.name).to eq('InputParameter1')
end

expect(stack).to receive(:create).and_return(true)

subject.create

Expand Down Expand Up @@ -74,6 +82,13 @@
subject.config.interactive = false
expect(stack).not_to receive(:create)

expect(ask_user_source).to receive(:get).ordered do |sp|
expect(sp.name).to eq('InputParameter1')
end
expect(ask_user_source).to receive(:get).ordered do |sp|
expect(sp.name).to eq('InputParameter4')
end

expect { subject.create }
.to raise_error(RuntimeError, 'The following parameters were not provided: InputParameter4')

Expand Down
1 change: 1 addition & 0 deletions spec/moonshot/plugins_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def post_create
expect(Moonshot::Stack).to receive(:new).and_return(stack)
template = Moonshot::YamlStackTemplate.new(fixture_path('empty1.yml'))
allow(stack).to receive(:template).and_return(template)
allow(stack).to receive(:parameters).and_return({})
end

it 'calls defined methods on plugins in order, providing them with a Moonshot::Resources' do
Expand Down

0 comments on commit 274e214

Please sign in to comment.