cfndsl-pipeline 0.1.2 → 0.1.4

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.
@@ -0,0 +1,41 @@
1
+ ---
2
+
3
+ TagStandard:
4
+ ServiceName:
5
+ Default: axt
6
+ Type: String
7
+ LogicalName: pServiceName
8
+ Label: Service pServiceName
9
+ EnvironmentID:
10
+ Default: dev1
11
+ AllowedPattern: (^dev|^sit|^prod)([-])?(\d{1,2})
12
+ Type: String
13
+ LogicalName: pEnvironmentID
14
+ Label: Environment ID
15
+ CostCentre:
16
+ Default: CC_DEVOPS
17
+ Type: String
18
+ LogicalName: pCostCentre
19
+ AllowedPattern: '[a-zA-Z0-9+-=._:/@ ]*'
20
+ Label: Cost Centre
21
+ Owner:
22
+ Default: 'Jane Doe'
23
+ Type: String
24
+ LogicalName: pOwner
25
+ Label: Owner
26
+ ApplicationID:
27
+ Default: CMDB-NS2010
28
+ Type: String
29
+ LogicalName: pApplicationID
30
+ Label:
31
+ AppCategory:
32
+ Default: B
33
+ Type: String
34
+ AllowedPattern: '[ABCD]'
35
+ LogicalName: pAppCategory
36
+ Label: Application Category
37
+ SupportGroup:
38
+ Default: 'DevOps Support'
39
+ Type: String
40
+ LogicalName: pSupportGroup
41
+ Label: Support Group
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
- #
4
3
  # The MIT License
5
4
  #
6
5
  # Copyright (c) 2019 Cam Maxwell (cameron.maxwell@gmail.com)
@@ -22,44 +21,45 @@
22
21
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
22
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
23
  # THE SOFTWARE.
25
- #
24
+ #
26
25
 
27
26
  require 'cfndsl'
27
+ require 'cfn-nag'
28
28
  require 'fileutils'
29
29
 
30
+ require_relative 'monkey-patches/cfndsl_patch'
31
+ require_relative 'monkey-patches/stdout_capture'
30
32
  require_relative 'options'
31
33
  require_relative 'params'
32
- require_relative 'monkey_patches'
33
- require_relative 'stdout_capture'
34
- require_relative 'run-cfndsl'
35
- require_relative 'run-cfn_nag'
36
- require_relative 'run-syntax'
34
+ require_relative 'exec_cfndsl'
35
+ require_relative 'exec_cfn_nag'
36
+ require_relative 'exec_syntax'
37
37
 
38
38
  module CfnDslPipeline
39
+ # Main pipeline
39
40
  class Pipeline
40
-
41
41
  attr_accessor :input_filename, :output_dir, :options, :base_name, :template, :output_filename, :output_file, :syntax_report
42
42
 
43
- def initialize (output_dir, options)
43
+ def initialize(output_dir, options)
44
44
  self.input_filename = ''
45
45
  self.output_file = nil
46
46
  self.template = nil
47
47
  self.options = options || nil
48
48
  self.syntax_report = []
49
49
  FileUtils.mkdir_p output_dir
50
- abort "Could not create output directory #{output_dir}" if Dir[output_dir] == nil
50
+ abort "Could not create output directory #{output_dir}" unless Dir[output_dir]
51
51
  self.output_dir = output_dir
52
52
  end
53
53
 
54
54
  def build(input_filename, cfndsl_extras)
55
- abort "Input file #{input_filename} doesn't exist!" if !File.file?(input_filename)
55
+ abort "Input file #{input_filename} doesn't exist!" unless File.file?(input_filename)
56
56
  self.input_filename = "#{input_filename}"
57
57
  self.base_name = File.basename(input_filename, '.*')
58
- self.output_filename = File.expand_path("#{self.output_dir}/#{self.base_name}.yaml")
58
+ self.output_filename = File.expand_path("#{output_dir}/#{base_name}.yaml")
59
59
  exec_cfndsl cfndsl_extras
60
- exec_syntax_validation if self.options.validate_syntax
61
- exec_dump_params if self.options.dump_deploy_params
62
- exec_cfn_nag if self.options.validate_cfn_nag
60
+ exec_syntax_validation if options.validate_syntax
61
+ exec_dump_params if options.dump_deploy_params
62
+ exec_cfn_nag if options.validate_cfn_nag
63
63
  end
64
64
  end
65
65
  end
@@ -0,0 +1,124 @@
1
+
2
+ require 'optparse'
3
+
4
+ module CfnDslPipeline
5
+ # Command Line Options processing
6
+ class CliOptions
7
+ attr_accessor :output, :template, :pipeline, :cfndsl_extras, :op
8
+
9
+ USAGE = "Usage: #{File.basename(__FILE__)} input file [ -o output_dir ] [ -b bucket ] OPTIONS [ include1 include2 etc.. ]"
10
+
11
+ def initialize
12
+ @output = './'
13
+ @cfndsl_extras = []
14
+ @pipeline = CfnDslPipeline::Options.new
15
+ parse && validate
16
+ end
17
+
18
+ private
19
+
20
+ # rubocop:disable Metrics/AbcSize
21
+ # rubocop:disable Metrics/MethodLength
22
+ def parse
23
+ @op = OptionParser.new do |opts|
24
+ opts.banner = USAGE
25
+
26
+ opts.on('-o', '--output dir', 'Optional output directory. Default is current directory') do |dir|
27
+ @output = dir
28
+ end
29
+
30
+ opts.on('-b', '--bucket', 'Optional Existing S3 bucket for cost estimation and large template syntax validation') do |bucket|
31
+ pipeline.validation_bucket = bucket
32
+ end
33
+
34
+ opts.on('-p', '--params', 'Create cloudformation deploy compatible params file') do
35
+ pipeline.dump_deploy_params = true
36
+ end
37
+
38
+ opts.on('-s', '--syntax', 'Enable syntax check') do
39
+ pipeline.validate_syntax = true
40
+ end
41
+
42
+ opts.on('--syntax-report', 'Save template syntax report') do
43
+ pipeline.save_syntax_report = true
44
+ end
45
+
46
+ opts.on('-a', '--audit', 'Enable cfn_nag audit') do
47
+ pipeline.validate_cfn_nag = false
48
+ end
49
+
50
+ opts.on('--audit-rule-dir', 'cfn_nag audit custom rules directory') do
51
+ pipeline.cfn_nag[:rule_directory] = true
52
+ end
53
+
54
+ opts.on('--audit-report', 'Save cfn_nag audit report') do
55
+ pipeline.save_audit_report = true
56
+ end
57
+
58
+ opts.on('--audit-debug', 'Enable cfn_nag rule debug output') do
59
+ pipeline.debug_audit = true
60
+ end
61
+
62
+ opts.on('-e', '--estimate-costs', 'Generate URL for AWS simple cost calculator') do
63
+ pipeline.estimate_cost = true
64
+ end
65
+
66
+ opts.on('-r', '--aws-region', 'AWS region to use. Default: ap-southeast-2') do |region|
67
+ pipeline.aws_region = region
68
+ end
69
+
70
+ opts.on_tail('-h', '--help', 'show this message') do
71
+ puts opts
72
+ exit
73
+ end
74
+
75
+ opts.on_tail('-d', '--debug', 'show pipeline debug messages') do
76
+ pipeline.debug = true
77
+ exit
78
+ end
79
+
80
+ opts.on_tail('-v', '--version', 'Show version') do
81
+ puts CfnDsl::Pipeline::VERSION
82
+ exit
83
+ end
84
+ end
85
+ @op.parse!
86
+
87
+ # first non-dash parameter is the mandatory input file
88
+ @template = ARGV.pop
89
+
90
+ ARGV.each do |arg|
91
+ @cfndsl_extras << [:yaml, arg]
92
+ end if ARGV.length > 0
93
+
94
+ pipeline
95
+ end
96
+ # rubocop:enable Metrics/AbcSize
97
+ # rubocop:enable Metrics/MethodLength
98
+
99
+ def fatal(msg)
100
+ puts msg
101
+ puts @op
102
+ exit 1
103
+ end
104
+
105
+ # rubocop:disable Metrics/CyclomaticComplexity
106
+ # rubocop:disable Metrics/PerceivedComplexity
107
+ def validate
108
+ # Exit on invalid option combinations
109
+ fatal 'Error: Input template file does not exist.' unless @template && File.file?(@template)
110
+
111
+ if @pipeline.save_syntax_report
112
+ fatal 'Error: save syntax report is set, but syntax validation was not enabled.' unless @pipeline.validate_syntax && !@pipeline.save_syntax_report
113
+ end
114
+ # rubocop:disable Style/GuardClause
115
+ if @pipeline.cfn_nag.rule_directory || @pipeline.debug_audit || @pipeline.save_audit_report
116
+ fatal 'Error: Audit options set, but audit was not enabled' unless @pipeline.validate_cfn_nag
117
+ fatal 'Error: cfn_nag rule directory does not exist' unless File.directory?(@pipeline.cfn_nag.rule_directory)
118
+ end
119
+ # rubocop:enable Style/GuardClause
120
+ end
121
+ # rubocop:enable Metrics/CyclomaticComplexity
122
+ # rubocop:enable Metrics/PerceivedComplexity
123
+ end
124
+ end
@@ -0,0 +1,58 @@
1
+ require 'cfn-nag'
2
+ require 'logging'
3
+ require 'colorize'
4
+
5
+ module CfnDslPipeline
6
+ # Add cfn_nag functions to pipeline
7
+ class Pipeline
8
+ def exec_cfn_nag
9
+ puts 'Auditing template with cfn-nag...'
10
+ configure_cfn_nag_logging
11
+ cfn_nag = CfnNag.new(config: options.cfn_nag)
12
+ result = cfn_nag.audit(cloudformation_string: template)
13
+ save_report result
14
+ display_report result
15
+ show_summary result
16
+ end
17
+
18
+ private
19
+
20
+ def configure_cfn_nag_logging
21
+ CfnNagLogging.configure_logging([:debug]) if options.debug_audit
22
+ end
23
+
24
+ def display_report(result)
25
+ ColoredStdoutResults.new.render([
26
+ {
27
+ filename: "#{@base_name}",
28
+ file_results: result
29
+ }
30
+ ])
31
+ end
32
+
33
+ def save_report(result)
34
+ return unless options.save_audit_report
35
+ report_data = Capture.capture do
36
+ SimpleStdoutResults.new.render([
37
+ {
38
+ filename: "#{@base_name}",
39
+ file_results: result
40
+ }
41
+ ])
42
+ end
43
+ filename = "#{output_dir}/#{base_name}.audit"
44
+ File.open(File.expand_path(filename), 'w').puts report_data['stdout']
45
+ puts "Saved audit report to #{filename}"
46
+ end
47
+
48
+ def show_summary(result)
49
+ if result[:failure_count] > 0
50
+ puts "Audit failed. #{result[:failure_count]} error(s) found ( ಠ ʖ̯ ಠ) ".red
51
+ elsif result[:violations].count > 0
52
+ puts "Audit passed with #{result[:warning_count]} warnings. (._.) ".yellow
53
+ else
54
+ puts 'Audit passed! ヽ( ゚ヮ゚)/ ヽ(´ー`)ノ'.green
55
+ end
56
+ end
57
+ end
58
+ end
@@ -3,16 +3,18 @@ require 'cfndsl/globals'
3
3
  require 'cfndsl/version'
4
4
 
5
5
  module CfnDslPipeline
6
+ #
6
7
  class Pipeline
7
- def exec_cfndsl(cfndsl_extras)
8
- print "Generating CloudFormation template...\n"
9
- model = CfnDsl.eval_file_with_extras("#{@input_filename}", cfndsl_extras)
8
+ def exec_cfndsl(cfndsl_extras)
9
+ puts 'Generating CloudFormation template...'
10
+
11
+ model = CfnDsl.eval_file_with_extras("#{@input_filename}", cfndsl_extras, (options.debug_cfndsl ? STDOUT : nil))
10
12
  @template = JSON.parse(model.to_json).to_yaml
11
13
  File.open(@output_filename, 'w') do |file|
12
14
  file.puts @template
13
15
  end
14
16
  @output_file = File.open(@output_filename)
15
- puts " #{@output_file.size} bytes written to #{@output_filename}"
17
+ puts "#{@output_file.size} bytes written to #{@output_filename}"
16
18
  end
17
19
  end
18
- end
20
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ require 'aws-sdk-cloudformation'
3
+ require 'aws-sdk-s3'
4
+ require 'uuid'
5
+
6
+ module CfnDslPipeline
7
+ #
8
+ class Pipeline
9
+ attr_accessor :s3_client
10
+
11
+ def initialize
12
+ self.s3_client = Aws::S3::Client.new(region: aws_region)
13
+ end
14
+ # rubocop:disable Metrics/AbcSize
15
+ def exec_syntax_validation
16
+ puts 'Validating template syntax...'
17
+ if options.estimate_cost || (output_file.size > 51_200)
18
+ puts 'Filesize is greater than 51200, or cost estimation required. Validating via S3 bucket '
19
+ uuid = UUID.new
20
+ bucket = determine_bucket
21
+ object_name = uuid.generate.to_s
22
+ upload_template(bucket, object_name)
23
+ self.syntax_report = s3_validate_syntax(bucket, object_name)
24
+ estimate_cost(bucket_name, object_name)
25
+ unless options.validation_bucket
26
+ puts 'Deleting temporary S3 bucket...'
27
+ bucket.delete!
28
+ end
29
+ else
30
+ self.syntax_report = local_validate_syntax
31
+ end
32
+ save_syntax_report
33
+ end
34
+ # rubocop:enable Metrics/AbcSize
35
+
36
+ private
37
+
38
+ def determine_bucket
39
+ if options.validation_bucket
40
+ bucket_name = options.validation_bucket
41
+ puts "Using existing S3 bucket #{bucket_name}..."
42
+ bucket = s3_client.bucket(options.validation_bucket)
43
+ else
44
+ bucket_name = "arch-code-#{uuid.generate}"
45
+ puts "Creating temporary S3 bucket #{bucket_name}..."
46
+ bucket = s3_client.bucket(bucket_name)
47
+ bucket.create
48
+ end
49
+ bucket
50
+ end
51
+
52
+ def save_syntax_report
53
+ return unless options.save_syntax_report
54
+ report_filename = "#{output_dir}/#{base_name}.report"
55
+ puts "Syntax validation report written to #{report_filename}"
56
+ File.open(File.expand_path(report_filename), 'w').puts syntax_report.to_hash.to_yaml
57
+ end
58
+
59
+ def upload_template(bucket, object_name)
60
+ puts 'Uploading template to temporary S3 bucket...'
61
+ object = bucket.object(object_name)
62
+ object.upload_file(output_file)
63
+ puts "https://s3.amazonaws.com/#{bucket_name}/#{object_name}"
64
+ end
65
+
66
+ def estimate_cost(bucket, object_name)
67
+ return unless options.estimate_cost
68
+ puts 'Estimate cost of template...'
69
+ client = Aws::CloudFormation::Client.new(region: options.aws_region)
70
+ costing = client.estimate_template_cost(template_url: "https://#{bucket.url}/#{object_name}")
71
+ puts "Cost Calculator URL is: #{costing.url}"
72
+ end
73
+
74
+ def s3_validate_syntax(bucket, object_name)
75
+ return unless options.validate_syntax
76
+ puts 'Validating template syntax in S3 Bucket...'
77
+ client = Aws::CloudFormation::Client.new(region: options.aws_region)
78
+ client.validate_template(template_url: "https://s3.amazonaws.com/#{bucket.url}/#{object_name}")
79
+ end
80
+
81
+ def local_validate_syntax
82
+ puts 'Validating template syntax locally...'
83
+ client = Aws::CloudFormation::Client.new(region: options.aws_region)
84
+ client.validate_template(template_body: template)
85
+ end
86
+ end
87
+ end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require 'cfndsl/globals'
4
3
  require 'cfndsl/version'
5
- PARAM_PROPS = %w[Description Default AllowedPattern AllowedValues].freeze
6
- HAS_PROPAGATABLE_TAGS = %w[CfnDsl::AWS::Types::AWS_AutoScaling_AutoScalingGroup].freeze
7
- HAS_MAPPED_TAGS = %w[CfnDsl::AWS::Types::AWS_Serverless_Function CfnDsl::AWS::Types::AWS_Serverless_SimpleTable CfnDsl::AWS::Types::AWS_Serverless_Application].freeze
4
+ require 'cfndsl/external_parameters'
5
+ require 'cfndsl/aws/cloud_formation_template'
6
+
7
+ PARAM_PROPS = %w([Description Default AllowedPattern AllowedValues]).freeze
8
+ HAS_PROPAGATABLE_TAGS = %w([CfnDsl::AWS::Types::AWS_AutoScaling_AutoScalingGroup]).freeze
9
+ # rubocop:disable Metrics/LineLength
10
+ HAS_MAPPED_TAGS = %w([CfnDsl::AWS::Types::AWS_Serverless_Function CfnDsl::AWS::Types::AWS_Serverless_SimpleTable CfnDsl::AWS::Types::AWS_Serverless_Application]).freeze
11
+ # rubocop:enable Metrics/LineLength
8
12
 
9
13
  # Automatically add Parameters for Tag values
10
14
  CfnDsl::CloudFormationTemplate.class_eval do
@@ -21,11 +25,20 @@ CfnDsl::CloudFormationTemplate.class_eval do
21
25
  send(key, props[key]) if props[key]
22
26
  end
23
27
  end
24
- end if external_parameters[:TagStandard].kind_of?(Hash)
28
+ end if external_parameters[:TagStandard].is_a?(Hash)
25
29
  end
26
30
  end
27
31
 
28
32
  module CfnDsl
33
+ # Add ability to reset params when being used in loops in rakefiles etc
34
+ class ExternalParameters
35
+ class << self
36
+ def reset
37
+ @parameters = self.class.defaults.clone
38
+ end
39
+ end
40
+ end
41
+
29
42
  # extends CfnDsl esource Properties to automatically substitute
30
43
  # FnSub recuraively
31
44
  class PropertyDefinition < JSONable
@@ -36,9 +49,8 @@ module CfnDsl
36
49
  def fix_substitutions(val)
37
50
  return val unless defined? val.class.to_s.downcase
38
51
  meth = "fix_#{val.class.to_s.downcase}"
39
- if respond_to?(meth.to_sym)
40
- return send(meth, val)
41
- end
52
+
53
+ return send(meth, val) if respond_to?(meth.to_sym)
42
54
  val
43
55
  end
44
56
 
@@ -62,27 +74,31 @@ module CfnDsl
62
74
  apply_tag_standard
63
75
  end
64
76
 
77
+ private
78
+
65
79
  def apply_tag_standard
66
80
  return unless defined? external_parameters[:TagStandard]
67
- return unless external_parameters[:TagStandard].kind_of?(Hash)
68
-
69
- resource_type = self.class.to_s
81
+ return unless external_parameters[:TagStandard].is_a?(Hash)
82
+ apply_tags(external_parameters[:TagStandard]) if defined? self.Tag
83
+ apply_tags_map(external_parameters[:TagStandard]) if HAS_MAPPED_TAGS.include? self.class.to_s
84
+ end
70
85
 
71
- if defined? self.Tag
72
- external_parameters[:TagStandard].each do |tag_name, props|
73
- send(:Tag) do
74
- Key tag_name.to_s
75
- Value Ref(props['LogicalName'] || tag_name)
76
- PropagateAtLaunch true if HAS_PROPAGATABLE_TAGS.include? resource_type
77
- end
78
- end
79
- elsif HAS_MAPPED_TAGS.include? resource_type
80
- tag_map = {}
81
- external_parameters[:TagStandard].each do |tag_name, props|
82
- tag_map[tag_name.to_s] = Ref(props['LogicalName'] || tag_name)
86
+ def apply_tags(tags)
87
+ tags.each do |tag_name, props|
88
+ send(:Tag) do
89
+ Key tag_name.to_s
90
+ Value Ref(props['LogicalName'] || tag_name)
91
+ PropagateAtLaunch true if HAS_PROPAGATABLE_TAGS.include? self.class.to_s
83
92
  end
84
- Tags tag_map
85
93
  end
86
94
  end
95
+
96
+ def apply_tags_map(tags)
97
+ tag_map = {}
98
+ tags.each do |tag_name, props|
99
+ tag_map[tag_name.to_s] = Ref(props['LogicalName'] || tag_name)
100
+ end
101
+ Tags tag_map
102
+ end
87
103
  end
88
104
  end