stack_master 1.6.0-x64-mingw32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|