aws-cft-tools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.gitignore +52 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/BEST-PRACTICES.md +136 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +8 -0
- data/LICENSE +15 -0
- data/README.md +118 -0
- data/Rakefile +17 -0
- data/USAGE.adoc +404 -0
- data/aws-cft-tools.gemspec +53 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/code.json +24 -0
- data/exe/aws-cft +176 -0
- data/lib/aws-cft-tools.rb +3 -0
- data/lib/aws_cft_tools.rb +31 -0
- data/lib/aws_cft_tools/aws_enumerator.rb +55 -0
- data/lib/aws_cft_tools/change.rb +66 -0
- data/lib/aws_cft_tools/client.rb +84 -0
- data/lib/aws_cft_tools/client/base.rb +40 -0
- data/lib/aws_cft_tools/client/cft.rb +93 -0
- data/lib/aws_cft_tools/client/cft/changeset_management.rb +109 -0
- data/lib/aws_cft_tools/client/cft/stack_management.rb +85 -0
- data/lib/aws_cft_tools/client/ec2.rb +136 -0
- data/lib/aws_cft_tools/client/templates.rb +84 -0
- data/lib/aws_cft_tools/deletion_change.rb +43 -0
- data/lib/aws_cft_tools/dependency_tree.rb +109 -0
- data/lib/aws_cft_tools/dependency_tree/nodes.rb +71 -0
- data/lib/aws_cft_tools/dependency_tree/variables.rb +37 -0
- data/lib/aws_cft_tools/errors.rb +25 -0
- data/lib/aws_cft_tools/runbook.rb +166 -0
- data/lib/aws_cft_tools/runbook/report.rb +30 -0
- data/lib/aws_cft_tools/runbooks.rb +16 -0
- data/lib/aws_cft_tools/runbooks/common/changesets.rb +30 -0
- data/lib/aws_cft_tools/runbooks/common/templates.rb +38 -0
- data/lib/aws_cft_tools/runbooks/deploy.rb +107 -0
- data/lib/aws_cft_tools/runbooks/deploy/reporting.rb +50 -0
- data/lib/aws_cft_tools/runbooks/deploy/stacks.rb +109 -0
- data/lib/aws_cft_tools/runbooks/deploy/templates.rb +37 -0
- data/lib/aws_cft_tools/runbooks/deploy/threading.rb +37 -0
- data/lib/aws_cft_tools/runbooks/diff.rb +28 -0
- data/lib/aws_cft_tools/runbooks/diff/context.rb +86 -0
- data/lib/aws_cft_tools/runbooks/diff/context/reporting.rb +87 -0
- data/lib/aws_cft_tools/runbooks/hosts.rb +43 -0
- data/lib/aws_cft_tools/runbooks/images.rb +43 -0
- data/lib/aws_cft_tools/runbooks/init.rb +86 -0
- data/lib/aws_cft_tools/runbooks/retract.rb +69 -0
- data/lib/aws_cft_tools/runbooks/retract/templates.rb +44 -0
- data/lib/aws_cft_tools/runbooks/stacks.rb +43 -0
- data/lib/aws_cft_tools/stack.rb +83 -0
- data/lib/aws_cft_tools/template.rb +177 -0
- data/lib/aws_cft_tools/template/dsl_context.rb +14 -0
- data/lib/aws_cft_tools/template/file_system.rb +62 -0
- data/lib/aws_cft_tools/template/metadata.rb +144 -0
- data/lib/aws_cft_tools/template/properties.rb +129 -0
- data/lib/aws_cft_tools/template_set.rb +120 -0
- data/lib/aws_cft_tools/template_set/array_methods.rb +63 -0
- data/lib/aws_cft_tools/template_set/closure.rb +77 -0
- data/lib/aws_cft_tools/template_set/dependencies.rb +55 -0
- data/lib/aws_cft_tools/template_set/each_slice_state.rb +58 -0
- data/lib/aws_cft_tools/version.rb +8 -0
- data/rubycritic.reek +3 -0
- metadata +321 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module AwsCftTools
|
6
|
+
class Client
|
7
|
+
##
|
8
|
+
# All of the business logic behind direct interaction with the AWS Template sources.
|
9
|
+
#
|
10
|
+
class Templates < Base
|
11
|
+
##
|
12
|
+
# Default template directory in the project.
|
13
|
+
DEFAULT_TEMPLATE_DIR = 'cloudformation/templates/'
|
14
|
+
|
15
|
+
##
|
16
|
+
# Default parameters directory in the project.
|
17
|
+
DEFAULT_PARAMETER_DIR = 'cloudformation/parameters/'
|
18
|
+
|
19
|
+
##
|
20
|
+
# Default set of file extensions that might contain templates.
|
21
|
+
#
|
22
|
+
TEMPLATE_FILE_EXTENSIONS = %w[.yaml .yml .json .rb].freeze
|
23
|
+
|
24
|
+
##
|
25
|
+
#
|
26
|
+
# @param options [Hash] client configuration
|
27
|
+
# @option options [String] :environment the operational environment in which to act
|
28
|
+
# @option options [String] :parameter_dir
|
29
|
+
# @option options [String] :region the AWS region in which to act
|
30
|
+
# @option options [Pathname] :root
|
31
|
+
# @option options [String] :template_dir
|
32
|
+
#
|
33
|
+
def initialize(options)
|
34
|
+
super({
|
35
|
+
template_dir: DEFAULT_TEMPLATE_DIR,
|
36
|
+
parameter_dir: DEFAULT_PARAMETER_DIR
|
37
|
+
}.merge(options))
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Lists all templates.
|
42
|
+
#
|
43
|
+
# @return AwsCftTools::TemplateSet
|
44
|
+
#
|
45
|
+
def templates
|
46
|
+
template_file_root = (options[:root] + options[:template_dir]).cleanpath
|
47
|
+
filtered_by_region(
|
48
|
+
filtered_by_environment(
|
49
|
+
all_templates(
|
50
|
+
template_file_root
|
51
|
+
)
|
52
|
+
)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def filtered_by_environment(set)
|
59
|
+
set.select { |template| template.environment?(options[:environment]) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def filtered_by_region(set)
|
63
|
+
set.select { |template| template.region?(options[:region]) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def all_templates(root)
|
67
|
+
AwsCftTools::TemplateSet.new(glob_templates(root)).tap do |set|
|
68
|
+
set.known_exports = options[:client].exports.map(&:name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def glob_templates(root)
|
73
|
+
Pathname.glob(root + '**/*')
|
74
|
+
.select { |file| TEMPLATE_FILE_EXTENSIONS.include?(file.extname) }
|
75
|
+
.map { |file| file_to_template(root, file) }
|
76
|
+
.select(&:template?)
|
77
|
+
end
|
78
|
+
|
79
|
+
def file_to_template(root, file)
|
80
|
+
AwsCftTools::Template.new(file.relative_path_from(root), options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
# Represents a change in a changeset.
|
5
|
+
class DeletionChange < Change
|
6
|
+
attr_reader :resource
|
7
|
+
|
8
|
+
###
|
9
|
+
#
|
10
|
+
# @param resource
|
11
|
+
#
|
12
|
+
def initialize(resource)
|
13
|
+
@resource = resource
|
14
|
+
end
|
15
|
+
|
16
|
+
###
|
17
|
+
# Return the action taken. For deletion, this is always +DELETE+.
|
18
|
+
#
|
19
|
+
# @return [String] +'DELETE'+ to indicate a deletion
|
20
|
+
#
|
21
|
+
def action
|
22
|
+
'DELETE'
|
23
|
+
end
|
24
|
+
|
25
|
+
###
|
26
|
+
# Return the status of this change as a replacement. For deletion, this is always +nil+.
|
27
|
+
#
|
28
|
+
# @return [nil]
|
29
|
+
#
|
30
|
+
def replacement
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
###
|
35
|
+
# Return the scopes of the change. For deletions, this is always +Resource+.
|
36
|
+
#
|
37
|
+
# @return [String] +'Resource'+
|
38
|
+
#
|
39
|
+
def scopes
|
40
|
+
'Resource'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
##
|
5
|
+
# = Dependency Tree
|
6
|
+
#
|
7
|
+
# Manage dependencies between CloudFormation templates based on exported and imported variables.
|
8
|
+
#
|
9
|
+
class DependencyTree
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
require_relative 'dependency_tree/nodes'
|
13
|
+
require_relative 'dependency_tree/variables'
|
14
|
+
|
15
|
+
attr_reader :filenames, :nodes, :variables
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@nodes = Nodes.new
|
19
|
+
@variables = Variables.new
|
20
|
+
@filenames = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# @!method undefined_variables
|
24
|
+
# @see AwsCftTools::DependencyTree::Variables#undefined_variables
|
25
|
+
# @!method defined_variables
|
26
|
+
# @see AwsCftTools::DependencyTree::Variables#defined_variables
|
27
|
+
def_delegators :variables, :undefined_variables, :defined_variables
|
28
|
+
# @!method exported
|
29
|
+
# @see AwsCftTools::DependencyTree::Variables#defined
|
30
|
+
def_delegator :variables, :defined, :exported
|
31
|
+
|
32
|
+
# @!method dependencies_for
|
33
|
+
# @see AwsCftTools::DependencyTree::Nodes#dependencies_for
|
34
|
+
# @!method dependents_for
|
35
|
+
# @see AwsCftTools::DependencyTree::Nodes#dependents_for
|
36
|
+
def_delegators :nodes, :dependencies_for, :dependents_for
|
37
|
+
|
38
|
+
##
|
39
|
+
# computes a topological sort and returns the filenames in that sort order
|
40
|
+
#
|
41
|
+
# @return [Array<String>]
|
42
|
+
#
|
43
|
+
def sort
|
44
|
+
nodes.tsort & filenames
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# notes that the given filename defines the given variable name
|
49
|
+
#
|
50
|
+
# @param filename [#to_s]
|
51
|
+
# @param variable [String]
|
52
|
+
#
|
53
|
+
def provided(filename, variable)
|
54
|
+
filename = filename.to_s
|
55
|
+
nodes.make_link(variable, filename)
|
56
|
+
@filenames |= [filename]
|
57
|
+
exported(variable)
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# notes that the given filename requires the given variable name to be defined before deployment
|
62
|
+
#
|
63
|
+
# @param filename [#to_s]
|
64
|
+
# @param variable [String]
|
65
|
+
#
|
66
|
+
def required(filename, variable)
|
67
|
+
filename = filename.to_s
|
68
|
+
nodes.make_link(filename, variable)
|
69
|
+
@filenames |= [filename]
|
70
|
+
variables.referenced(variable)
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# links two nodes in a directed fashion
|
75
|
+
#
|
76
|
+
# The template named by _from_ provides resources required by the template named by _to_.
|
77
|
+
#
|
78
|
+
# @param from [#to_s]
|
79
|
+
# @param to [#to_s]
|
80
|
+
#
|
81
|
+
def linked(from, to)
|
82
|
+
linker = "#{from}$$#{to}"
|
83
|
+
provided(from, linker)
|
84
|
+
required(to, linker)
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# finds a subset of the given set that has no dependencies outside the set
|
89
|
+
#
|
90
|
+
# @param set [Array<T>]
|
91
|
+
# @return [Array<T>]
|
92
|
+
#
|
93
|
+
def closed_subset(set)
|
94
|
+
# list all nodes that have no dependents outside the set
|
95
|
+
close_subset(set, &method(:dependents_for))
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def close_subset(set, &block)
|
101
|
+
return [] unless block_given?
|
102
|
+
set - items_outside_subset(set, &block)
|
103
|
+
end
|
104
|
+
|
105
|
+
def items_outside_subset(set)
|
106
|
+
set.select { |node| (yield(node) - set).any? }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class DependencyTree
|
5
|
+
##
|
6
|
+
# Manages a list of nodes or vertices. Edges pass from a filename node to a variable node or from
|
7
|
+
# a variable node to a filename node, but never from a filename node to a filename node or from
|
8
|
+
# a variable node to a variable node.
|
9
|
+
#
|
10
|
+
class Nodes
|
11
|
+
include TSort
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@nodes = default_hash
|
15
|
+
@inverse_nodes = default_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Computes the direct dependencies of a node that are of the same type as the node. If the node
|
20
|
+
# is a filename, then the returned nodes will be filenames. Likewise with variable names.
|
21
|
+
#
|
22
|
+
# @param node [String]
|
23
|
+
# @return [Array<String>]
|
24
|
+
#
|
25
|
+
def dependencies_for(node)
|
26
|
+
double_hop(@nodes, node.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Computes the things dependent on the given node. If the node is a filename, then the returned
|
31
|
+
# nodes will be filenames. Likewise with variable names.
|
32
|
+
#
|
33
|
+
# @param node [String]
|
34
|
+
# @return [Array<String>]
|
35
|
+
#
|
36
|
+
def dependents_for(node)
|
37
|
+
double_hop(@inverse_nodes, node.to_s)
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Draws a directed link from +from+ to +to+.
|
42
|
+
#
|
43
|
+
# @param from [String]
|
44
|
+
# @param to [String]
|
45
|
+
def make_link(from, to)
|
46
|
+
@nodes[from] << to
|
47
|
+
@inverse_nodes[to] << from
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def double_hop(set, node)
|
53
|
+
# we hop from a filename to a variable child to a filename child
|
54
|
+
# or from a variable to a filename child to a variable child
|
55
|
+
set[node].flat_map { |neighbor| set[neighbor] }.uniq
|
56
|
+
end
|
57
|
+
|
58
|
+
def tsort_each_node(&block)
|
59
|
+
@nodes.each_key(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def tsort_each_child(node, &block)
|
63
|
+
@nodes[node].each(&block) if @nodes.include?(node)
|
64
|
+
end
|
65
|
+
|
66
|
+
def default_hash
|
67
|
+
Hash.new { |hash, key| hash[key] = [] }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class DependencyTree
|
5
|
+
##
|
6
|
+
# Manage list of defined/undefined variables
|
7
|
+
#
|
8
|
+
class Variables
|
9
|
+
attr_reader :undefined_variables, :defined_variables
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@undefined_variables = []
|
13
|
+
@defined_variables = []
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Notes that the given variable name is provided either by the CloudFormation environment or by
|
18
|
+
# another template.
|
19
|
+
#
|
20
|
+
# @param name [String]
|
21
|
+
#
|
22
|
+
def defined(name)
|
23
|
+
@undefined_variables -= [name]
|
24
|
+
@defined_variables |= [name]
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Notes that the given variable name is used as an input into a template.
|
29
|
+
#
|
30
|
+
# @param name [String]
|
31
|
+
#
|
32
|
+
def referenced(name)
|
33
|
+
@undefined_variables |= [name] unless @defined_variables.include?(name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
##
|
5
|
+
# Provides a root class for matching any internal exceptions.
|
6
|
+
#
|
7
|
+
class ToolingException < StandardError; end
|
8
|
+
|
9
|
+
##
|
10
|
+
# Exception when a needed environment variable is not provided. Intended for use in
|
11
|
+
# parameter files.
|
12
|
+
class IncompleteEnvironmentError < ToolingException; end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Exception raised when the template or parameter source file can not be read and parsed.
|
16
|
+
class ParseException < ToolingException; end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Exception raised when the AWS SDK raises an error interacting with the AWS API.
|
20
|
+
class CloudFormationError < ToolingException; end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Exception raised when an expected resolvable template dependency can not be satisfied.
|
24
|
+
class UnsatisfiedDependencyError < ToolingException; end
|
25
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
##
|
5
|
+
# This is the base class for runbooks.
|
6
|
+
#
|
7
|
+
# A runbook is a command that can be accessed via the `aws-cft` script. A runbook is implemented as a
|
8
|
+
# subclass of this class.
|
9
|
+
#
|
10
|
+
# == Callbacks
|
11
|
+
# The AwsCftTools::CLI uses callbacks to manage runbook-specific options and behaviors.
|
12
|
+
#
|
13
|
+
# == Helpers
|
14
|
+
#
|
15
|
+
# The various helpers make managing logic flow much easier. Rather than having to be aware of how the
|
16
|
+
# different modes (+verbose+, +change+, +noop+) interplay and are configured, you can use the
|
17
|
+
# methods in this section to annotate the flow with your intent.
|
18
|
+
#
|
19
|
+
# @abstract Subclass and override {.default_options}, {.options}, and {#run} to implement
|
20
|
+
# a custom Runbook class.
|
21
|
+
class Runbook
|
22
|
+
require_relative 'runbook/report'
|
23
|
+
|
24
|
+
##
|
25
|
+
# The AwsCftTools::Client instance used by the runbook.
|
26
|
+
#
|
27
|
+
attr_reader :client
|
28
|
+
|
29
|
+
##
|
30
|
+
# The configuration options passed in to the runbook.
|
31
|
+
#
|
32
|
+
attr_reader :options
|
33
|
+
|
34
|
+
##
|
35
|
+
# Recognized configuration options depend on the runbook but include any options valid for
|
36
|
+
# AwsCftTools::Client.
|
37
|
+
#
|
38
|
+
# Modes are selected by various configuration options:
|
39
|
+
#
|
40
|
+
# +:verbose+:: provide more narrative as the runbook executes
|
41
|
+
# +:noop+:: do nothing that could modify state
|
42
|
+
# +:change+:: do nothing that could permanently modify state, though some modifications are permitted
|
43
|
+
# in order to examine what might change if permanent changes were made
|
44
|
+
#
|
45
|
+
# @param configuration [Hash] Various options passed to the AwsCftTools::Client.
|
46
|
+
# @return AwsCftTools::Runbook
|
47
|
+
#
|
48
|
+
def initialize(configuration = {})
|
49
|
+
@options = configuration
|
50
|
+
@client = AwsCftTools::Client.new(options)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @!group Callbacks
|
54
|
+
|
55
|
+
##
|
56
|
+
# A callback to implement the runbook actions. Nothing is passed in or returned.
|
57
|
+
#
|
58
|
+
# @return void
|
59
|
+
#
|
60
|
+
def run; end
|
61
|
+
|
62
|
+
##
|
63
|
+
# An internal wrapper around +#run+ to catch credential errors and print a useful message.
|
64
|
+
#
|
65
|
+
def _run
|
66
|
+
run
|
67
|
+
rescue Aws::Errors::MissingCredentialsError
|
68
|
+
puts 'Unable to access AWS without valid credentials. Either define a default credential' \
|
69
|
+
' profile or use the -p option to specify an AWS credential profile.'
|
70
|
+
end
|
71
|
+
|
72
|
+
# @!group Helpers
|
73
|
+
|
74
|
+
##
|
75
|
+
# @param description [String] an optional description of the operation
|
76
|
+
# @yield runs the block if not in +noop+ mode
|
77
|
+
# @return void
|
78
|
+
#
|
79
|
+
# Defines an operation that may or may not be narrated and that should not be run if in +noop+ mode.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# operation("considering the change" ) do
|
83
|
+
# checking("seeing what changes") { check_what_changes }
|
84
|
+
# doing("committing the change") { make_the_change }
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
def operation(description = nil)
|
88
|
+
narrative(description)
|
89
|
+
return if options[:noop]
|
90
|
+
|
91
|
+
yield if block_given?
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# @param description [String] an optional description of the check
|
96
|
+
# @yield runs the block if in +check+ mode and not in +noop+ mode
|
97
|
+
# @return void
|
98
|
+
#
|
99
|
+
# Runs the given block when in +check+ mode and not in +noop+ mode. Outputs the description if the
|
100
|
+
# block is run.
|
101
|
+
#
|
102
|
+
# @example (see #operation)
|
103
|
+
#
|
104
|
+
def checking(description = nil)
|
105
|
+
return if options[:noop] || !options[:check]
|
106
|
+
output(description)
|
107
|
+
yield if block_given?
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# @param description [String] an optional description of the action
|
112
|
+
# @yield runs the block if not in +check+ or +noop+ mode
|
113
|
+
# @return void
|
114
|
+
#
|
115
|
+
# Runs the given block when not in +check+ or +noop+ mode. Outputs the description if the block is run.
|
116
|
+
#
|
117
|
+
# @example (see #operation)
|
118
|
+
#
|
119
|
+
def doing(description = nil)
|
120
|
+
return if options[:noop] || options[:check]
|
121
|
+
output(description)
|
122
|
+
yield if block_given?
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# @param description [String] an optional narrative string
|
127
|
+
# @return void
|
128
|
+
#
|
129
|
+
# Prints out the given description to stdout. If in +noop+ mode, +" (noop)"+ is appended.
|
130
|
+
#
|
131
|
+
def narrative(description = nil)
|
132
|
+
return unless description
|
133
|
+
if options[:noop]
|
134
|
+
puts "#{description} (noop)"
|
135
|
+
else
|
136
|
+
puts description
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# @param description [String] an optional verbose description
|
142
|
+
# @yield runs the block if in +verbose+ mode
|
143
|
+
# @return void
|
144
|
+
#
|
145
|
+
# Prints out the given description and runs the block if in +verbose+ mode. This is useful if the
|
146
|
+
# verbose narrative might be computationally expensive.
|
147
|
+
#
|
148
|
+
# @example
|
149
|
+
# detail do
|
150
|
+
# ... run some extensive processing to summarize stuff
|
151
|
+
# puts "The results are #{interesting}."
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
def detail(description = nil)
|
155
|
+
return unless options[:verbose]
|
156
|
+
output(description)
|
157
|
+
yield if block_given?
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def output(description)
|
163
|
+
puts description if description
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|