stack_master 1.6.0-x64-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +548 -0
- data/bin/stack_master +17 -0
- data/lib/stack_master.rb +159 -0
- data/lib/stack_master/aws_driver/cloud_formation.rb +41 -0
- data/lib/stack_master/aws_driver/s3.rb +68 -0
- data/lib/stack_master/change_set.rb +109 -0
- data/lib/stack_master/cli.rb +208 -0
- data/lib/stack_master/command.rb +57 -0
- data/lib/stack_master/commands/apply.rb +221 -0
- data/lib/stack_master/commands/delete.rb +53 -0
- data/lib/stack_master/commands/diff.rb +31 -0
- data/lib/stack_master/commands/events.rb +39 -0
- data/lib/stack_master/commands/init.rb +111 -0
- data/lib/stack_master/commands/list_stacks.rb +20 -0
- data/lib/stack_master/commands/outputs.rb +31 -0
- data/lib/stack_master/commands/resources.rb +33 -0
- data/lib/stack_master/commands/status.rb +46 -0
- data/lib/stack_master/commands/terminal_helper.rb +28 -0
- data/lib/stack_master/commands/validate.rb +17 -0
- data/lib/stack_master/config.rb +133 -0
- data/lib/stack_master/ctrl_c.rb +4 -0
- data/lib/stack_master/paged_response_accumulator.rb +29 -0
- data/lib/stack_master/parameter_loader.rb +49 -0
- data/lib/stack_master/parameter_resolver.rb +98 -0
- data/lib/stack_master/parameter_resolvers/ami_finder.rb +36 -0
- data/lib/stack_master/parameter_resolvers/env.rb +18 -0
- data/lib/stack_master/parameter_resolvers/latest_ami.rb +19 -0
- data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +18 -0
- data/lib/stack_master/parameter_resolvers/parameter_store.rb +31 -0
- data/lib/stack_master/parameter_resolvers/secret.rb +52 -0
- data/lib/stack_master/parameter_resolvers/security_group.rb +22 -0
- data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +31 -0
- data/lib/stack_master/parameter_resolvers/stack_output.rb +76 -0
- data/lib/stack_master/prompter.rb +21 -0
- data/lib/stack_master/resolver_array.rb +35 -0
- data/lib/stack_master/security_group_finder.rb +28 -0
- data/lib/stack_master/sns_topic_finder.rb +26 -0
- data/lib/stack_master/sparkle_formation/compile_time/allowed_pattern_validator.rb +35 -0
- data/lib/stack_master/sparkle_formation/compile_time/allowed_values_validator.rb +37 -0
- data/lib/stack_master/sparkle_formation/compile_time/definitions_validator.rb +33 -0
- data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +32 -0
- data/lib/stack_master/sparkle_formation/compile_time/max_length_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/max_size_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/min_length_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/min_size_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/number_validator.rb +35 -0
- data/lib/stack_master/sparkle_formation/compile_time/parameters_validator.rb +27 -0
- data/lib/stack_master/sparkle_formation/compile_time/state_builder.rb +32 -0
- data/lib/stack_master/sparkle_formation/compile_time/string_validator.rb +33 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_builder.rb +40 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_validator.rb +40 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_validator_factory.rb +41 -0
- data/lib/stack_master/sparkle_formation/template_file.rb +115 -0
- data/lib/stack_master/stack.rb +105 -0
- data/lib/stack_master/stack_definition.rb +103 -0
- data/lib/stack_master/stack_differ.rb +111 -0
- data/lib/stack_master/stack_events/fetcher.rb +38 -0
- data/lib/stack_master/stack_events/presenter.rb +27 -0
- data/lib/stack_master/stack_events/streamer.rb +68 -0
- data/lib/stack_master/stack_states.rb +34 -0
- data/lib/stack_master/stack_status.rb +61 -0
- data/lib/stack_master/template_compiler.rb +30 -0
- data/lib/stack_master/template_compilers/cfndsl.rb +13 -0
- data/lib/stack_master/template_compilers/json.rb +22 -0
- data/lib/stack_master/template_compilers/sparkle_formation.rb +71 -0
- data/lib/stack_master/template_compilers/yaml.rb +14 -0
- data/lib/stack_master/template_utils.rb +31 -0
- data/lib/stack_master/test_driver/cloud_formation.rb +193 -0
- data/lib/stack_master/test_driver/s3.rb +34 -0
- data/lib/stack_master/testing.rb +9 -0
- data/lib/stack_master/utils.rb +50 -0
- data/lib/stack_master/validator.rb +33 -0
- data/lib/stack_master/version.rb +3 -0
- metadata +457 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'empty_validator'
|
2
|
+
require_relative 'string_validator'
|
3
|
+
require_relative 'number_validator'
|
4
|
+
require_relative 'allowed_values_validator'
|
5
|
+
require_relative 'allowed_pattern_validator'
|
6
|
+
require_relative 'max_length_validator'
|
7
|
+
require_relative 'min_length_validator'
|
8
|
+
require_relative 'max_size_validator'
|
9
|
+
require_relative 'min_size_validator'
|
10
|
+
|
11
|
+
module StackMaster
|
12
|
+
module SparkleFormation
|
13
|
+
module CompileTime
|
14
|
+
class ValueValidatorFactory
|
15
|
+
|
16
|
+
VALIDATORS_TYPES = [
|
17
|
+
EmptyValidator,
|
18
|
+
StringValidator,
|
19
|
+
NumberValidator,
|
20
|
+
AllowedValuesValidator,
|
21
|
+
AllowedPatternValidator,
|
22
|
+
MaxLengthValidator,
|
23
|
+
MinLengthValidator,
|
24
|
+
MaxSizeValidator,
|
25
|
+
MinSizeValidator
|
26
|
+
]
|
27
|
+
|
28
|
+
def initialize(name, definition, parameter)
|
29
|
+
@name = name
|
30
|
+
@definition = definition
|
31
|
+
@parameter = parameter
|
32
|
+
end
|
33
|
+
|
34
|
+
def build
|
35
|
+
VALIDATORS_TYPES.map { |validator| validator.new(@name, @definition, @parameter)}
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'sparkle_formation'
|
2
|
+
require 'erubis'
|
3
|
+
|
4
|
+
module StackMaster
|
5
|
+
module SparkleFormation
|
6
|
+
TemplateFileNotFound = ::Class.new(StandardError)
|
7
|
+
|
8
|
+
class SfEruby < Erubis::Eruby
|
9
|
+
include Erubis::ArrayEnhancer
|
10
|
+
|
11
|
+
def add_expr(src, code, indicator)
|
12
|
+
case indicator
|
13
|
+
when '='
|
14
|
+
src << " #{@bufvar} << (" << code << ');'
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class TemplateContext < AttributeStruct
|
22
|
+
include ::SparkleFormation::SparkleAttribute
|
23
|
+
include ::SparkleFormation::SparkleAttribute::Aws
|
24
|
+
include ::SparkleFormation::Utils::TypeCheckers
|
25
|
+
|
26
|
+
def self.build(vars, prefix)
|
27
|
+
::Class.new(self).tap do |klass|
|
28
|
+
vars.each do |key, value|
|
29
|
+
klass.send(:define_method, key) do
|
30
|
+
value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end.new(vars, prefix)
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(vars, prefix)
|
38
|
+
self._camel_keys = true
|
39
|
+
@vars = vars
|
40
|
+
@prefix = prefix
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_var?(var_key)
|
44
|
+
@vars.include?(var_key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def render(file_name, vars = {})
|
48
|
+
Template.render(@prefix, file_name, vars)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Splits up long strings with multiple lines in them to multiple strings
|
53
|
+
# in the CF array. Makes the compiled template and diffs more readable.
|
54
|
+
class CloudFormationLineFormatter
|
55
|
+
def self.format(template)
|
56
|
+
new(template).format
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(template)
|
60
|
+
@template = template
|
61
|
+
end
|
62
|
+
|
63
|
+
def format
|
64
|
+
@template.flat_map do |lines|
|
65
|
+
lines = lines.to_s if Symbol === lines
|
66
|
+
if String === lines
|
67
|
+
newlines = []
|
68
|
+
lines.count("\n").times do
|
69
|
+
newlines << "\n"
|
70
|
+
end
|
71
|
+
newlines = lines.split("\n").map do |line|
|
72
|
+
"#{line}#{newlines.pop}"
|
73
|
+
end
|
74
|
+
if lines.starts_with?("\n")
|
75
|
+
newlines.insert(0, "\n")
|
76
|
+
end
|
77
|
+
newlines
|
78
|
+
else
|
79
|
+
lines
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
module Template
|
86
|
+
def self.render(prefix, file_name, vars)
|
87
|
+
file_path = File.join(::SparkleFormation.sparkle_path, prefix, file_name)
|
88
|
+
template = File.read(file_path)
|
89
|
+
template_context = TemplateContext.build(vars, prefix)
|
90
|
+
compiled_template = SfEruby.new(template).evaluate(template_context)
|
91
|
+
CloudFormationLineFormatter.format(compiled_template)
|
92
|
+
rescue Errno::ENOENT => e
|
93
|
+
Kernel.raise TemplateFileNotFound, "Could not find template file at path: #{file_path}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
module JoinedFile
|
98
|
+
def _joined_file(file_name, vars = {})
|
99
|
+
join!(Template.render('joined_file', file_name, vars))
|
100
|
+
end
|
101
|
+
alias_method :joined_file!, :_joined_file
|
102
|
+
end
|
103
|
+
|
104
|
+
module UserDataFile
|
105
|
+
def _user_data_file(file_name, vars = {})
|
106
|
+
base64!(join!(Template.render('user_data', file_name, vars)))
|
107
|
+
end
|
108
|
+
alias_method :user_data_file!, :_user_data_file
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
SparkleFormation::SparkleAttribute::Aws.send(:include, StackMaster::SparkleFormation::UserDataFile)
|
114
|
+
SparkleFormation::SparkleAttribute::Aws.send(:include, StackMaster::SparkleFormation::JoinedFile)
|
115
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module StackMaster
|
2
|
+
class Stack
|
3
|
+
attr_reader :stack_name,
|
4
|
+
:region,
|
5
|
+
:stack_id,
|
6
|
+
:stack_status,
|
7
|
+
:parameters,
|
8
|
+
:template_body,
|
9
|
+
:template_format,
|
10
|
+
:role_arn,
|
11
|
+
:notification_arns,
|
12
|
+
:outputs,
|
13
|
+
:stack_policy_body,
|
14
|
+
:tags,
|
15
|
+
:files
|
16
|
+
|
17
|
+
include Utils::Initializable
|
18
|
+
|
19
|
+
def template_default_parameters
|
20
|
+
TemplateUtils.template_hash(template).fetch('Parameters', {}).inject({}) do |result, (parameter_name, description)|
|
21
|
+
result[parameter_name] = description['Default']
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def parameters_with_defaults
|
27
|
+
template_default_parameters.merge(parameters)
|
28
|
+
end
|
29
|
+
|
30
|
+
def missing_parameters?
|
31
|
+
parameters_with_defaults.any? do |key, value|
|
32
|
+
value == nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.find(region, stack_name)
|
37
|
+
cf = StackMaster.cloud_formation_driver
|
38
|
+
cf_stack = cf.describe_stacks(stack_name: stack_name).stacks.first
|
39
|
+
return unless cf_stack
|
40
|
+
parameters = cf_stack.parameters.inject({}) do |params_hash, param_struct|
|
41
|
+
params_hash[param_struct.parameter_key] = param_struct.parameter_value
|
42
|
+
params_hash
|
43
|
+
end
|
44
|
+
template_body ||= cf.get_template(stack_name: stack_name, template_stage: 'Original').template_body
|
45
|
+
template_format = TemplateUtils.identify_template_format(template_body)
|
46
|
+
stack_policy_body ||= cf.get_stack_policy(stack_name: stack_name).stack_policy_body
|
47
|
+
outputs = cf_stack.outputs
|
48
|
+
|
49
|
+
new(region: region,
|
50
|
+
stack_name: stack_name,
|
51
|
+
stack_id: cf_stack.stack_id,
|
52
|
+
parameters: parameters,
|
53
|
+
template_body: template_body,
|
54
|
+
template_format: template_format,
|
55
|
+
outputs: outputs,
|
56
|
+
role_arn: cf_stack.role_arn,
|
57
|
+
notification_arns: cf_stack.notification_arns,
|
58
|
+
stack_policy_body: stack_policy_body,
|
59
|
+
stack_status: cf_stack.stack_status)
|
60
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.generate(stack_definition, config)
|
65
|
+
parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
|
66
|
+
template_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:template_parameters])
|
67
|
+
compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters])
|
68
|
+
template_body = TemplateCompiler.compile(config, stack_definition.template_file_path, compile_time_parameters, stack_definition.compiler_options)
|
69
|
+
template_format = TemplateUtils.identify_template_format(template_body)
|
70
|
+
stack_policy_body = if stack_definition.stack_policy_file_path
|
71
|
+
File.read(stack_definition.stack_policy_file_path)
|
72
|
+
end
|
73
|
+
new(region: stack_definition.region,
|
74
|
+
stack_name: stack_definition.stack_name,
|
75
|
+
tags: stack_definition.tags,
|
76
|
+
parameters: template_parameters,
|
77
|
+
template_body: template_body,
|
78
|
+
template_format: template_format,
|
79
|
+
role_arn: stack_definition.role_arn,
|
80
|
+
notification_arns: stack_definition.notification_arns,
|
81
|
+
stack_policy_body: stack_policy_body)
|
82
|
+
end
|
83
|
+
|
84
|
+
def max_template_size(use_s3)
|
85
|
+
return TemplateUtils::MAX_S3_TEMPLATE_SIZE if use_s3
|
86
|
+
TemplateUtils::MAX_TEMPLATE_SIZE
|
87
|
+
end
|
88
|
+
|
89
|
+
def too_big?(use_s3 = false)
|
90
|
+
template.size > max_template_size(use_s3)
|
91
|
+
end
|
92
|
+
|
93
|
+
def aws_parameters
|
94
|
+
Utils.hash_to_aws_parameters(parameters)
|
95
|
+
end
|
96
|
+
|
97
|
+
def aws_tags
|
98
|
+
Utils.hash_to_aws_tags(tags)
|
99
|
+
end
|
100
|
+
|
101
|
+
def template
|
102
|
+
@template ||= TemplateUtils.maybe_compressed_template_body(template_body)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module StackMaster
|
2
|
+
class StackDefinition
|
3
|
+
attr_accessor :region,
|
4
|
+
:stack_name,
|
5
|
+
:template,
|
6
|
+
:tags,
|
7
|
+
:role_arn,
|
8
|
+
:notification_arns,
|
9
|
+
:base_dir,
|
10
|
+
:template_dir,
|
11
|
+
:secret_file,
|
12
|
+
:stack_policy_file,
|
13
|
+
:additional_parameter_lookup_dirs,
|
14
|
+
:s3,
|
15
|
+
:files,
|
16
|
+
:compiler_options
|
17
|
+
|
18
|
+
include Utils::Initializable
|
19
|
+
|
20
|
+
def initialize(attributes = {})
|
21
|
+
@additional_parameter_lookup_dirs = []
|
22
|
+
@compiler_options = {}
|
23
|
+
@notification_arns = []
|
24
|
+
@s3 = {}
|
25
|
+
@files = []
|
26
|
+
super
|
27
|
+
@template_dir ||= File.join(@base_dir, 'templates')
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
self.class === other &&
|
32
|
+
@region == other.region &&
|
33
|
+
@stack_name == other.stack_name &&
|
34
|
+
@template == other.template &&
|
35
|
+
@tags == other.tags &&
|
36
|
+
@role_arn == other.role_arn &&
|
37
|
+
@notification_arns == other.notification_arns &&
|
38
|
+
@base_dir == other.base_dir &&
|
39
|
+
@secret_file == other.secret_file &&
|
40
|
+
@stack_policy_file == other.stack_policy_file &&
|
41
|
+
@additional_parameter_lookup_dirs == other.additional_parameter_lookup_dirs &&
|
42
|
+
@s3 == other.s3 &&
|
43
|
+
@compiler_options == other.compiler_options
|
44
|
+
end
|
45
|
+
|
46
|
+
def template_file_path
|
47
|
+
File.expand_path(File.join(template_dir, template))
|
48
|
+
end
|
49
|
+
|
50
|
+
def files_dir
|
51
|
+
File.join(base_dir, 'files')
|
52
|
+
end
|
53
|
+
|
54
|
+
def s3_files
|
55
|
+
files.inject({}) do |hash, file|
|
56
|
+
path = File.join(files_dir, file)
|
57
|
+
hash[file] = {
|
58
|
+
path: path,
|
59
|
+
body: File.read(path)
|
60
|
+
}
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def s3_template_file_name
|
66
|
+
return template if ['.json', '.yaml', '.yml'].include?(File.extname(template))
|
67
|
+
Utils.change_extension(template, 'json')
|
68
|
+
end
|
69
|
+
|
70
|
+
def parameter_files
|
71
|
+
[ default_parameter_file_path, region_parameter_file_path, additional_parameter_lookup_file_paths ].flatten.compact
|
72
|
+
end
|
73
|
+
|
74
|
+
def stack_policy_file_path
|
75
|
+
File.join(base_dir, 'policies', stack_policy_file) if stack_policy_file
|
76
|
+
end
|
77
|
+
|
78
|
+
def s3_configured?
|
79
|
+
!s3.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def additional_parameter_lookup_file_paths
|
85
|
+
return unless additional_parameter_lookup_dirs
|
86
|
+
additional_parameter_lookup_dirs.map do |a|
|
87
|
+
Dir.glob(File.join(base_dir, 'parameters', a, "#{underscored_stack_name}.y*ml"))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def region_parameter_file_path
|
92
|
+
Dir.glob(File.join(base_dir, 'parameters', "#{region}", "#{underscored_stack_name}.y*ml"))
|
93
|
+
end
|
94
|
+
|
95
|
+
def default_parameter_file_path
|
96
|
+
Dir.glob(File.join(base_dir, 'parameters', "#{underscored_stack_name}.y*ml"))
|
97
|
+
end
|
98
|
+
|
99
|
+
def underscored_stack_name
|
100
|
+
stack_name.gsub('-', '_')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require "diffy"
|
2
|
+
|
3
|
+
module StackMaster
|
4
|
+
class StackDiffer
|
5
|
+
def initialize(proposed_stack, current_stack)
|
6
|
+
@proposed_stack = proposed_stack
|
7
|
+
@current_stack = current_stack
|
8
|
+
end
|
9
|
+
|
10
|
+
def proposed_template
|
11
|
+
return @proposed_stack.template_body unless @proposed_stack.template_format == :json
|
12
|
+
JSON.pretty_generate(JSON.parse(@proposed_stack.template_body))
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_template
|
16
|
+
return '' unless @current_stack
|
17
|
+
return @current_stack.template_body unless @current_stack.template_format == :json
|
18
|
+
JSON.pretty_generate(TemplateUtils.template_hash(@current_stack.template_body))
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_parameters
|
22
|
+
if @current_stack
|
23
|
+
YAML.dump(sort_params(@current_stack.parameters_with_defaults))
|
24
|
+
else
|
25
|
+
''
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def proposed_parameters
|
30
|
+
# **** out any secret parameters in the current stack.
|
31
|
+
params = @proposed_stack.parameters_with_defaults
|
32
|
+
if @current_stack
|
33
|
+
noecho_keys.each do |key|
|
34
|
+
params[key] = "****"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
YAML.dump(sort_params(params))
|
38
|
+
end
|
39
|
+
|
40
|
+
def body_different?
|
41
|
+
body_diff != ''
|
42
|
+
end
|
43
|
+
|
44
|
+
def body_diff
|
45
|
+
@body_diff ||= Diffy::Diff.new(current_template, proposed_template, context: 7, include_diff_info: true).to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def params_different?
|
49
|
+
params_diff != ''
|
50
|
+
end
|
51
|
+
|
52
|
+
def params_diff
|
53
|
+
@param_diff ||= Diffy::Diff.new(current_parameters, proposed_parameters, {}).to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def output_diff
|
57
|
+
display_diff('Stack', body_diff)
|
58
|
+
display_diff('Parameters', params_diff)
|
59
|
+
unless noecho_keys.empty?
|
60
|
+
StackMaster.stdout.puts " * can not tell if NoEcho parameters are different."
|
61
|
+
end
|
62
|
+
StackMaster.stdout.puts "No stack found" if @current_stack.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
def noecho_keys
|
66
|
+
if @current_stack
|
67
|
+
@current_stack.parameters_with_defaults.select do |key, value|
|
68
|
+
value == "****"
|
69
|
+
end.keys
|
70
|
+
else
|
71
|
+
[]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def display_diff(thing, diff)
|
78
|
+
StackMaster.stdout.print "#{thing} diff: "
|
79
|
+
if diff == ''
|
80
|
+
StackMaster.stdout.puts "No changes"
|
81
|
+
else
|
82
|
+
StackMaster.stdout.puts
|
83
|
+
diff.each_line do |line|
|
84
|
+
if line.start_with?('+')
|
85
|
+
StackMaster.stdout.print colorize(line, :green)
|
86
|
+
elsif line.start_with?('-')
|
87
|
+
StackMaster.stdout.print colorize(line, :red)
|
88
|
+
else
|
89
|
+
StackMaster.stdout.print line
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def sort_params(hash)
|
96
|
+
hash.sort.to_h
|
97
|
+
end
|
98
|
+
|
99
|
+
def colorize(text, color)
|
100
|
+
if colorize?
|
101
|
+
text.colorize(color)
|
102
|
+
else
|
103
|
+
text
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def colorize?
|
108
|
+
ENV.fetch('COLORIZE') { 'true' } == 'true'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|